diff --git a/apps/cs/src/pages/apilog/ui/apilog-page.tsx b/apps/cs/src/pages/apilog/ui/apilog-page.tsx index 532752840..a81275198 100644 --- a/apps/cs/src/pages/apilog/ui/apilog-page.tsx +++ b/apps/cs/src/pages/apilog/ui/apilog-page.tsx @@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { Button, - DateRangePicker, Input, + InputDatePicker, Label, Select, SelectContent, @@ -21,7 +21,7 @@ import { ENDPOINT } from "@/shared/config"; import { columns } from "@/pages/apilog/model/table-columns"; type DateRange = { - from: Date | undefined; + from?: Date | undefined; to?: Date | undefined; }; @@ -35,6 +35,7 @@ function ApiLogPage() { pageIndex: 0, pageSize: 10, }); + const [presetDateRange, setPresetDateRange] = useState(""); const [dateRange, setDateRange] = useState(defaultDateRange); const [userId, setUserId] = useState(""); const [apiHttpPmhd, setApiHttpPmhd] = useState(""); @@ -84,8 +85,10 @@ function ApiLogPage() { ...searchParams, userId, apiHttpPmhd, - dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"), - dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"), + dmndDtStart: dateRange.from + ? dayjs(dateRange.from).format("YYYY-MM-DD") + : "", + dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "", }); }; @@ -126,6 +129,7 @@ function ApiLogPage() { default: presetDateRange = undefined; } + setPresetDateRange(value); setDateRange(presetDateRange || defaultDateRange); }; @@ -134,23 +138,31 @@ function ApiLogPage() {
- setDateRange(value || defaultDateRange)} - > -
- -
-
+ + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + /> { + const response = await client.get( + ENDPOINT.CMS_SERVICE.DASHBOARD, + ); + return response.data; + } +} + +export const DashboardApi = new DashboardApiService(); diff --git a/apps/cs/src/pages/dashboard/lib/utils.ts b/apps/cs/src/pages/dashboard/lib/utils.ts new file mode 100644 index 000000000..669402cb8 --- /dev/null +++ b/apps/cs/src/pages/dashboard/lib/utils.ts @@ -0,0 +1,49 @@ +import type { ChartData, ChartDataset } from "chart.js"; +import { getCssVariableValue } from "@common/components"; +import type { + MenuStatisticsContent, + SiteStatisticsContent, +} from "../model/dashboard-interface"; + +export function processSiteAccessData( + siteAccessList: SiteStatisticsContent[], +): ChartData<"line">["datasets"] { + const siteGroups: Record = siteAccessList.reduce( + (acc, item) => { + if (!acc[item.siteNm]) { + acc[item.siteNm] = { + label: item.siteNm, + data: Array(24).fill(0), // 24시간 데이터 초기화 + borderColor: getCssVariableValue( + `--chart-${Math.floor(Math.random() * 5) + 1}`, + ), + }; + } + + // 시간대별 접속 카운트 누적 + const hourIndex = parseInt(item.accessHour, 10); + if (!isNaN(hourIndex) && hourIndex >= 0 && hourIndex < 24) { + acc[item.siteNm]!.data[hourIndex] = item.accessCnt; + } + + return acc; + }, + {} as Record, + ); + + return Object.values(siteGroups) as ChartData<"line">["datasets"]; +} + +export function processMenuAccessData( + menuAccessList: MenuStatisticsContent[], +): ChartData<"bar">["datasets"] { + const data = menuAccessList.map((item) => item.accessCnt); + + return [ + { + label: "메뉴별 접속 횟수", + data, + backgroundColor: "rgb(32, 148, 250)", + }, + ] as ChartData<"bar">["datasets"]; +} diff --git a/apps/cs/src/pages/dashboard/model/chart-data.ts b/apps/cs/src/pages/dashboard/model/chart-data.ts new file mode 100644 index 000000000..0d55f9bc7 --- /dev/null +++ b/apps/cs/src/pages/dashboard/model/chart-data.ts @@ -0,0 +1,172 @@ +import { type ChartData, type ChartOptions } from "chart.js"; + +const commonData = { + labels: ["1", "2", "3", "4", "5", "6", "7"], + datasets: [ + { + fill: true, + pointRadius: 0, + tension: 0.5, + }, + ], +}; + +const commonOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false, + }, + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + }, + }, +}; + +export const chartData: Record< + string, + { data: ChartData<"line" | "bar">; options: ChartOptions<"line" | "bar"> } +> = { + memberChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [8, 10, 7, 12, 9, 13, 12], + borderColor: "rgba(255, 255, 255, 0.6)", + backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + scales: { + ...commonOptions.scales, + y: { + ...commonOptions.scales.y, + min: 5, + max: 15, + }, + }, + }, + }, + postChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [5200, 5350, 5450, 5520, 5600, 5650, 5678], + borderColor: "rgba(86, 138, 255, 1)", + backgroundColor: "rgba(86, 138, 255, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + visitorChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [1800, 2100, 1950, 2300, 2150, 2250, 2345], + borderColor: "rgba(149, 131, 252)", + backgroundColor: "rgba(149, 131, 252, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + apiChartData: { + data: { + ...commonData, + datasets: [ + { + ...commonData.datasets[0], + data: [41000, 42500, 43200, 44100, 44800, 45200, 45678], + borderColor: "rgba(220, 103, 206)", + backgroundColor: "rgba(220, 103, 206, 0.1)", + }, + ], + }, + options: { + ...commonOptions, + }, + }, + siteAccessChartData: { + data: { + labels: Array.from({ length: 24 }, (_, i) => + i.toString().padStart(2, "0"), + ), + datasets: [], + }, + options: { + ...commonOptions, + plugins: { + legend: { + position: "top", + }, + tooltip: { + mode: "index", + intersect: false, + }, + }, + scales: { + x: { + title: { + text: "시간대", + }, + }, + y: { + title: { + text: "접속자 수", + }, + beginAtZero: true, + }, + }, + }, + }, + menuAccessChartData: { + data: { + labels: [], + datasets: [], + }, + options: { + ...commonOptions, + plugins: { + legend: { + position: "top", + }, + tooltip: { + mode: "index", + intersect: false, + }, + }, + scales: { + x: { + title: { + text: "메뉴명", + }, + }, + y: { + title: { + text: "접속 횟수", + }, + beginAtZero: true, + }, + }, + }, + }, +}; diff --git a/apps/cs/src/pages/dashboard/model/dashboard-interface.ts b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts new file mode 100644 index 000000000..9e038eef7 --- /dev/null +++ b/apps/cs/src/pages/dashboard/model/dashboard-interface.ts @@ -0,0 +1,32 @@ +export interface SiteStatisticsContent { + siteId: string; + siteNm: string; + accessHour: string; + accessCnt: number; +} + +export interface MenuStatisticsContent { + menuCd: number; + menuNm: string; + accessCnt: number; +} + +export interface Dashboard { + memberTotCnt: number; + memberTodayCnt: number; + pstTotCnt: number; + pstTodayCnt: number; + accessTodayCnt: number; + accessBefDayPer: number; + apiTodayCnt: number; + waitMemberCnt: number; + waitCntnCnt: number; + notiList: []; + schdlList: []; + siteAccessList: SiteStatisticsContent[]; + menuAccessList: MenuStatisticsContent[]; +} + +export interface DashboardService { + getDashboard(): Promise; +} diff --git a/apps/cs/src/pages/dashboard/model/table-columns.tsx b/apps/cs/src/pages/dashboard/model/table-columns.tsx new file mode 100644 index 000000000..4d015e2dc --- /dev/null +++ b/apps/cs/src/pages/dashboard/model/table-columns.tsx @@ -0,0 +1,74 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Badge } from "@common/components"; +import type { RecentPost } from "@/entities/post"; + +const noticePostTableColumns: ColumnDef[] = [ + { + header: "번호", + cell: ({ row, table }) => { + if (row.original.upndFixYn === "Y") { + return ( + + + + ); + } + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + }, + { + accessorKey: "wrtrNm", + header: "작성자", + }, + { + accessorKey: "regDt", + header: "작성일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + }, + { + accessorKey: "inqNocs", + header: "조회수", + }, +]; + +const recentPostTableColumns: ColumnDef[] = [ + { + header: "번호", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + }, + { + accessorKey: "wrtrNm", + header: "작성자", + }, + { + accessorKey: "regDt", + header: "작성일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + }, + { + accessorKey: "inqNocs", + header: "조회수", + }, +]; + +export { noticePostTableColumns, recentPostTableColumns }; diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx index 4dc693361..4d657a5fc 100644 --- a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -1,5 +1,409 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel } from "@tanstack/react-table"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Legend, + Tooltip, + type ChartData, +} from "chart.js"; +import { Bar, Line } from "react-chartjs-2"; +import { PostApi, type RecentPost } from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { DashboardApi } from "../api/dashboard-service"; +import { chartData } from "../model/chart-data"; +import { + noticePostTableColumns, + recentPostTableColumns, +} from "../model/table-columns"; +import { processMenuAccessData, processSiteAccessData } from "../lib/utils"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Filler, + Legend, + Tooltip, +); + function DashboardPage() { - return <>; + const router = useRouter(); + const { data } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.DASHBOARD], + queryFn: () => DashboardApi.getDashboard(), + }); + + const noticePostQuery = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => + PostApi.getRecentPostList({ + page: 0, + size: 5, + keywordType: "bbsTypeCd", + keyword: "BBSNOTICE", + }), + }); + + const recentPostQuery = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], + queryFn: () => + PostApi.getRecentPostList({ + page: 0, + size: 5, + }), + }); + + const handleNoticePostRowClick = (row: RecentPost) => { + router.push(`/bbs/bbsnotice/${row.bbsId}/${row.pstNo}`); + }; + + const handleRecentPostRowClick = (row: RecentPost) => { + router.push( + `/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`, + ); + }; + + const { tableComponent: noticePostTableComponent } = useDataTable({ + query: noticePostQuery, + tableOptions: { + columns: noticePostTableColumns, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + variant: "line", + onRowClick: handleNoticePostRowClick, + }, + }); + + const { tableComponent: recentPostTableComponent } = useDataTable({ + query: recentPostQuery, + tableOptions: { + columns: recentPostTableColumns, + getCoreRowModel: getCoreRowModel(), + }, + dataTableOptions: { + variant: "line", + onRowClick: handleRecentPostRowClick, + }, + }); + + return ( +
+
+ {/* 첫번째 라인 */} +
+ {/* 총 회원수 카드 */} +
+
+
+

총 회원수

+
+ {data?.memberTotCnt ?? "-"} +
+

+ 금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명 +

+
+ {/* 차트 영역 */} +
+ } + options={chartData.memberChartData!.options} + /> +
+
+
+ {/* 전체 게시물 카드 */} +
+
+
+

전체 게시물

+
+ {data?.pstTotCnt ?? "-"} +
+

+ 금일 +{data?.pstTodayCnt ?? "-"} 등록 +

+
+ {/* 차트 영역 */} +
+ } + options={chartData.postChartData!.options} + /> +
+
+
+ {/* 오늘 접속자 수 카드 */} +
+
+
+

오늘 접속자 수

+
+ {data?.accessTodayCnt ?? "-"} +
+

+ 전일 대비 +{data?.accessBefDayPer ?? "-"}% +

+
+ {/* 차트 영역 */} +
+ } + options={chartData.visitorChartData!.options} + /> +
+
+
+ {/* API 호출 건수 카드 */} +
+
+
+

API 호출 건수

+
+ {data?.apiTodayCnt ?? "-"} +
+

금일 누적

+
+ {/* 차트 영역 */} +
+ } + options={chartData.apiChartData!.options} + /> +
+
+
+
+ + {/* 두번째 라인 */} +
+ {/* 승인 대기 알림 */} +
+
+

승인 대기 알림

+
+
+
+ {/* 사용자 승인 */} +
+
+
+ + + + +
+ 사용자 승인 +
+
+
+ {data?.waitMemberCnt}건 +
+
+ 신규 가입 승인 대기 +
+
+
+ {/* 매뉴 승인 */} +
+
+
+ + + + + + +
+ 메뉴 승인 +
+
+
3건
+
+ 매뉴 수정 승인 대기 +
+
+
+ {/* 컨텐츠 승인 */} +
+
+
+ + + + +
+ 컨텐츠 승인 +
+
+
+ {data?.waitCntnCnt}건 +
+
+ 컨텐츠 수정 승인 대기 +
+
+
+
+
+
+
+ {/* 실시간 알림 */} +
+
+

실시간 알림

+
+
+
+
+ 신규 사용자 가입 + 1분 전 +
+
+ 메뉴 수정 요청 + 5분 전 +
+
+ 게시물 신고 + 10분 전 +
+
+ 컨텐츠 승인 요청 + 15분 전 +
+
+
+
+ {/* 일정 알림 */} +
+
+

일정 알림

+
+
+
+
+ 3월 정기 시스템 점검 + 내일 +
+
+ 서버 업데이트 예정 + 3일 후 +
+
+ 백업 스케줄 + 5일 후 +
+
+ 정기 보안 점검 + 7일 후 +
+
+
+
+
+
+ + {/* 세번째 라인 */} +
+ {/* 접속자 통계 */} +
+
+

접속자 통계

+
+
+ +
+
+ {/* 메뉴별 접속 통계 */} +
+
+

메뉴별 접속 통계

+
+
+ item.menuNm) || [], + datasets: processMenuAccessData(data?.menuAccessList || []), + }} + options={chartData.menuAccessChartData!.options} + /> +
+
+
+ + {/* 네번째 라인 */} +
+ {/* 공지사항 */} +
+
+

공지사항

+
+ {noticePostTableComponent} +
+ {/* 신규 게시글 */} +
+
+

신규 게시글

+
+ {recentPostTableComponent} +
+
+
+
+ ); } export { DashboardPage }; diff --git a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx index 4bad7cba6..a9748b28d 100644 --- a/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx +++ b/apps/cs/src/pages/loginlog/ui/loginlog-page.tsx @@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { Button, - DateRangePicker, Input, + InputDatePicker, Label, Select, SelectContent, @@ -22,7 +22,7 @@ import { ENDPOINT } from "@/shared/config"; import { columns } from "@/pages/loginlog/model/table-columns"; type DateRange = { - from: Date | undefined; + from?: Date | undefined; to?: Date | undefined; }; @@ -32,13 +32,12 @@ function LoginlogPage() { to: dayjs().toDate(), }; - const [dateRange, setDateRange] = useState(defaultDateRange); - const [isManualLoading, setIsManualLoading] = useState(false); - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); + const [presetDateRange, setPresetDateRange] = useState(""); + const [dateRange, setDateRange] = useState(defaultDateRange); const [userId, setUserId] = useState(""); const [errOcrnYn, setErrOcrnYn] = useState("N"); const [searchParams, setSearchParams] = useState({ @@ -48,6 +47,7 @@ function LoginlogPage() { searchDateTo: dayjs().format("YYYY-MM-DD"), sort: "lgnDt,desc", }); + const [isManualLoading, setIsManualLoading] = useState(false); const query = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination], @@ -88,8 +88,12 @@ function LoginlogPage() { ...searchParams, userId, errOcrnYn, - searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"), - searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"), + searchDateFrom: dateRange.from + ? dayjs(dateRange.from).format("YYYY-MM-DD") + : "", + searchDateTo: dateRange.to + ? dayjs(dateRange.to).format("YYYY-MM-DD") + : "", }); query.refetch().finally(() => { setIsManualLoading(false); @@ -133,6 +137,7 @@ function LoginlogPage() { default: presetDateRange = undefined; } + setPresetDateRange(value); setDateRange(presetDateRange || defaultDateRange); }; @@ -141,23 +146,31 @@ function LoginlogPage() {
- setDateRange(value || defaultDateRange)} - > -
- -
-
+ + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + /> (defaultDateRange); const [inqInfoKeyword, setInqInfoKeyword] = useState(""); const [searchParams, setSearchParams] = useState({ @@ -85,8 +86,10 @@ function PrivacyLogPage() { setSearchParams({ ...searchParams, inqInfoKeyword, - inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"), - inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"), + inqDtStart: dateRange.from + ? dayjs(dateRange.from).format("YYYY-MM-DD") + : "", + inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "", }); }; @@ -125,6 +128,7 @@ function PrivacyLogPage() { default: presetDateRange = undefined; } + setPresetDateRange(value); setDateRange(presetDateRange || defaultDateRange); }; @@ -133,30 +137,37 @@ function PrivacyLogPage() {
- setDateRange(value || defaultDateRange)} - > -
- -
-
+ + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + /> setInqInfoKeyword(e.target.value)} /> - diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx index 8f188eced..1ad451ae7 100644 --- a/apps/cs/src/pages/recent/ui/recent-page.tsx +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -12,8 +12,8 @@ import { } from "@tanstack/react-table"; import { Button, - DateRangePicker, Input, + InputDatePicker, Label, Select, SelectContent, @@ -37,7 +37,7 @@ import { columns } from "../model/table-columns"; import { MovePostDialog } from "./move-post-dialog"; type DateRange = { - from: Date | undefined; + from?: Date | undefined; to?: Date | undefined; }; @@ -51,22 +51,21 @@ function RecentPage() { const { confirm } = useConfirm(); const alert = useAlertStore((state) => state.setMessage); - const [dateRange, setDateRange] = useState({ - from: undefined, - to: undefined, - }); const [tableSort, setTableSort] = useState("regDt,desc"); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); const [rowSelection, setRowSelection] = useState({}); - + const [presetDateRange, setPresetDateRange] = useState(""); + const [dateRange, setDateRange] = useState({ + from: undefined, + to: undefined, + }); const [keywordType, setKeywordType] = useState(""); const [keyword, setKeyword] = useState(""); const [bbsId, setBbsId] = useState(null); const [delYn, setDelYn] = useState("N"); - const [searchParams, setSearchParams] = useState({ keywordType: "", keyword: "", @@ -193,6 +192,7 @@ function RecentPage() { default: presetDateRange = undefined; } + setPresetDateRange(value); setDateRange(presetDateRange); }; @@ -250,21 +250,31 @@ function RecentPage() {
- -
- -
-
- + + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + /> - - - - - - - 1개월 - 3개월 - 6개월 - - -
- + + { + setDateRange({ ...dateRange, from: date }); + setPresetDateRange(""); + }} + /> + ~ + { + setDateRange({ ...dateRange, to: date }); + setPresetDateRange(""); + }} + />
diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 120a45b2e..6db62f1a0 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -23,6 +23,7 @@ export const ENDPOINT = { PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs", }, CMS_SERVICE: { + DASHBOARD: "/cms-service/api/v1/dashboard", SITES: "/cms-service/api/v1/sites", IPS: "/cms-service/api/v1/site-ips", DOMAINS: "/cms-service/api/v1/site-dmns", diff --git a/apps/dm/next-env.d.ts b/apps/dm/next-env.d.ts new file mode 100644 index 000000000..1b3be0840 --- /dev/null +++ b/apps/dm/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/fo/next-env.d.ts b/apps/fo/next-env.d.ts new file mode 100644 index 000000000..1b3be0840 --- /dev/null +++ b/apps/fo/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/sn/.env.production b/apps/sn/.env.production index 0ede28ed8..fd98438d7 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -1,13 +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://172.29.143.87:8080/geoserver -NEXT_PUBLIC_LX_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver_swl -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_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)/map/components/common/table.ts b/apps/sn/app/(map-layout)/map/components/common/table.ts index cabb95649..68448af71 100644 --- a/apps/sn/app/(map-layout)/map/components/common/table.ts +++ b/apps/sn/app/(map-layout)/map/components/common/table.ts @@ -376,7 +376,7 @@ export const MsSwageMhlCol = [ { id: 'vrtc_len', title: '세로길이(m)' }, // { id: 'ang_dir', title: '표고' }, { id: 'altd_vl', title: '표고(EL.m)' }, - { id: 'dpthlVl', title: '심도(m)' }, + { id: 'dpthl_vl', title: '심도(m)' }, // { id: 'inst_mng_no', title: '기관관리번호' } ]; export const MsSwageMhlCol2 = [ @@ -507,7 +507,7 @@ export const ExclnSewagePumpCol2 = [ // { id: 'instMngNo', title: '기관관리번호' }, ] // 오수펌프장 -export const DrtwSewagePumpCol1 = [ +export const DrtwSewagePumpCol = [ // { id: 'fid', title: 'FID' }, // { id: 'geom', title: 'geometry' }, // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, @@ -574,16 +574,16 @@ export const stofcbrCol = [ { id: 'drtn_angl', title: '방향각도(°)' }, ] export const stofcbrCol2 = [ - // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, - { id: 'pbadmsEmdCd', title: '시설관리번호' }, - // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, { id: 'mapshtNo', title: '도엽번호' }, - // { id: 'mng_inst_cd', title: '관리기관코드' }, + // { id: 'mngInstCd', title: '관리기관코드' }, { id: 'instlYmd', title: '설치일자' }, - // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, { id: 'worflwWallHgt', title: '월류벽높이(m)' }, - // { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' }, - // { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' }, + // { id: 'mhlExstYnCd', title: '맨홀존재여부코드' }, + // { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' }, { id: 'swageMhlMngNo', title: '하수맨홀관리번호' }, { id: 'drtnAngl', title: '방향각도(°)' }, ] diff --git a/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx index 3e8229310..079557972 100644 --- a/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/featureClickEventPopup.tsx @@ -6,7 +6,7 @@ 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, DrtwSewagePumpCol1, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "./components/common/table"; +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"; @@ -14,7 +14,6 @@ import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; import { codeMapper } from "./components/common/codeMapper"; -// TODO: const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; const [layerNm, setLayerNm] = useState(""); // 레이어 이름 상태 @@ -59,59 +58,124 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, let result = null; const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기 let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수 - layers.forEach((layer: 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; }); - // console.log("featureLayer.get('lyrTyNm');", featureLayer.get('lyrTyNm')); - // SearchModal 클릭 이벤트 - if (featureLayer.get('lyrNm') === null) { - setIsClicked(false); - if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; - else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; - else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') result = '우수토실'; - else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; - else if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') result = '하수관로'; - else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') { - const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장'); - console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val); - if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) { - setIsDrtwSewagePump(false); - } else { - setIsDrtwSewagePump(true); - } - result = '하수펌프장'; - } - else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') result = '하수맨홀'; - } - else { + + if (featureData['ol_uid']) { // Gis 레이어 클릭 이벤트 setIsClicked(true); - const lyrTyNm = featureLayer.get('lyrTyNm'); - if (lyrTyNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; - else if (lyrTyNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; - else if (lyrTyNm === 'tb_ms_stofcbr') result = '우수토실'; - else if (lyrTyNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; - else if (lyrTyNm === 'tb_ms_swage_ppl') result = '하수관로'; - else if (lyrTyNm === 'tb_ms_swage_pmstn') { + 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 === '오수펌프장'); - console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val); if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) { setIsDrtwSewagePump(false); - } else { - setIsDrtwSewagePump(true); + setHeadlist(ExclnSewagePumpCol); } + else { + setIsDrtwSewagePump(true); + setHeadlist(DrtwSewagePumpCol); + } + setLayerSpecificNm(featureData.values_.fclt_mng_no); result = '하수펌프장'; } - else if (lyrTyNm === 'tb_ms_swage_mhl') 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; - // return featureLayer.get('lyrNm'); // 레이어 이름 출력 }; useEffect(() => { @@ -122,8 +186,10 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 setLayerNm(selectedLayerName); // 상태 업데이트 - console.log('gis data', featureData.values_); - const data = featureData.values_; + // console.log('gis data', featureData.values_); + let data; + if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때 + else data = featureData; // SearchModal 클릭 이벤트일 때 // 하수맨홀인 경우 용도코드를 텍스트로 변환 if (selectedLayerName === '하수맨홀' && data) { @@ -139,16 +205,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, // 🎯 기본 데이터 포맷팅 const formattedData = formatDisplayData(data); - if (featureData?.values_?.fclt_mng_no) setLayerSpecificNm(featureData.values_.fclt_mng_no); - else if (featureData?.values_?.swtrmt_fclt_cd) setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); - else setLayerSpecificNm(featureData?.values_?.manage_no); setFetchData(formattedData); setTableData(formattedData); switch (selectedLayerName) { case '하수관로': - if (isClicked) setHeadlist(MsSwagePplCol) - else setHeadlist(MsSwagePplCol2) setButtonChk(true) // ✅ codeMapper 사용하여 API 결과도 변환 @@ -156,12 +217,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, layerName: selectedLayerName, data: { ...formattedData } })); - // setFetchData(mappedData); - // setTableData(mappedData); break; case '하수맨홀': - if (isClicked) setHeadlist(MsSwageMhlCol) - else setHeadlist(MsSwageMhlCol2) setButtonChk(true) // ✅ codeMapper 사용하여 API 결과도 변환 @@ -169,46 +226,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, layerName: selectedLayerName, data: { ...formattedData } })); - // setFetchData(mappedData); - // setTableData(mappedData); break; case '하수펌프장': - if (isDrtwSewagePump) { - if (isClicked) setHeadlist(DrtwSewagePumpCol1) - else setHeadlist(DrtwSewagePumpCol2) - } else { - if (isClicked) setHeadlist(ExclnSewagePumpCol) - else setHeadlist(ExclnSewagePumpCol2) - } - setButtonChk(true) setTableData(codeMapper({ layerName: selectedLayerName, data: { ...formattedData } })); - // setFetchData(mappedData); - // setTableData(mappedData); break; case '하수처리시설': - if (isClicked) setHeadlist(SwtrmtFcltCol) - else setHeadlist(SwtrmtFcltCol2) setButtonChk(false) break; case '하수처리구역': - if (isClicked) setHeadlist(SwtrmtPrcsZoneCol) - else setHeadlist(SwtrmtPrcsZoneCol2) setButtonChk(false) break; case '우수토실': - if (isClicked) setHeadlist(stofcbrCol) - else setHeadlist(stofcbrCol2) setButtonChk(true) setIsMainSsc(false) break; case '중점우수토실': - setHeadlist(untrtSwgRainCol1) - setHeadlist2(untrtSwgRainCol2) setButtonChk(false) setIsMainSsc(true) // 🎯 processedData도 포맷팅하여 설정 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..9bcb6e2c8 --- /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 = ({ + // 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 ( +
+ {/* 구분 */} +
+
+ + setBasicType(String(value)) + } + value={basicType} + selectContentClass="z-[101]" + /> +
+
+ {/* 분기처리: 하수관로 */} + {basicType === "tb_ms_swage_ppl" && ( + + )} + {/* 분기처리: 하수처리시설 */} + {basicType === "tb_il_swage_prcs_fclt" && ( + + )} + {/* 분기처리: 하수처리구역 */} + {basicType === "tb_ms_swage_prcs_zone" && ( + + )} + {/* 분기처리: 하수맨홀 */} + {basicType === "tb_ms_swage_mhl" && ( + + )} + {/* 분기처리: 하수펌프장 */} + {basicType === "tb_ms_swage_pmstn" && ( + + )} + {/* 분기처리: 우수토실 */} + {basicType === "tb_ms_stofcbr" && ( +
+ {/* 설치년도 */} +
+ setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ )} +
+ ); +}; + +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..a3ec069e9 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx @@ -0,0 +1,162 @@ +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", text: "보수 및 갱신(개대체)" }, + { val: "201", text: "보수 및 갱신(유지보수)" }, + { val: "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: "bizNm", title: "사업명" }, + { id: "flfmtBgngYmd", title: "수행시작일자" }, + { id: "flfmtEndYmd", title: "수행종료일자" }, + { id: "flfmtYr", title: "수행년도" }, + { id: "bizTyCd", title: "사업유형" }, + { id: "bizAmt", title: "사업금액" }, +]; +// 운영정보 - 보수 및 갱신 결과표 +export const operationMaintenanceTableHeadlist = [ + { 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: "fcltMngNo", title: "관리번호" }, + { id: "usgCd", title: "용도" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmt", title: "관경(mm)" }, + { id: "prlg", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, +]; +// 운영정보 - 점검 및 진단 결과표 +export const operationInspectionTableHeadlist = [ + { 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 = ({ + open, + workType, + var1, + var2, + yearFilter, + handleSearch, + // onOpenChange, + // setTableData, + // setTableCount, + setWorkType, + setVar1, + setVar2, + setYearFilter, +}) => { + return ( +
+
+ ({ + val: w.val, + text: w.text, + }))} + onSelectValue={(val) => setWorkType(String(val))} + value={workType} + selectContentClass="z-[101]" + /> +
+
+ + { setVar1(e.target.value); }} + /> + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="수행기간" + endLabel="" + /> + { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ) +}; + +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..9dac307e9 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -0,0 +1,109 @@ +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input } from '@common/components'; +import React from 'react'; +//import { useDcTree, flattenFolders, useSggCd, useCityCd } from './components/searchModalReportApi'; +import { type SelectDataType } from '../commonStore'; +import { useCityCd, useSggCd } from '../components/searchModalReportApi'; +// import { useBizByFcltStore } from "../../../store/StoreProvider"; + + +// // 하수도정비 기본계획 보고서 +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; + gubun: string; + sidos: SelectDataType[]; + ctpvCd: string | number; + sigungu: string; + filteredSiguns: SelectDataType[]; + yearFilter: { start: string; end: string; }; + handleSearch: () => void; + handleCtpvCdChange: (val: string | number) => void; + // onOpenChange?: (open: boolean) => void; + // onFeatureSelect?: (row: any) => void; + // setTableData?: (data: any[]) => void; + // setTableCount?: (count: number) => void; + setYearFilter: (filter: { start: string; end: string }) => void; + setGubun: (gubun: string) => void; + setSigungu: (sigungu: string) => void; +} + +const ReportTab: React.FC = ({ + open, + gubun, + sidos, + ctpvCd, + sigungu, + filteredSiguns = [{ val: '0', text: '전체', sido: '0' }], + yearFilter, + handleSearch, + handleCtpvCdChange, + // onOpenChange, + // onFeatureSelect, + // setTableData, + // setTableCount, + setYearFilter, + setGubun, + setSigungu, +}) => { + + + return ( +
+
+ + setSigungu(String(val))} + selectContentClass="z-[101]" + /> +
+
+ + { setGubun(e.target.value); }} + /> + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="승인년도" + endLabel="" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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 index 299b5181c..426b606aa 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -1,18 +1,17 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import axios from 'axios'; -import { UiModal, UiTable, DatePickerRange, SelectBox, UITab } from '@common/business_components/ui'; -import { Button, Input, Label } from '@common/components'; -import { SelectBox2 } from '@common/business_components/ui/select/index-2'; -import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; -import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox'; -import { MsSwagePplCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2, MsSwageMhlCol2, ExclnSewagePumpCol2, stofcbrCol2 } from '../components/common/table'; 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 { useDcTree, flattenFolders, useSggCd, useCityCd } from './components/searchModalReportApi'; -import { useDcTree, flattenFolders, useSggCd, useCityCd, addFileName, getFileDownload } from '../components/searchModalReportApi'; -import { useCityCdStore, type SelectDataType } from '../commonStore'; -import { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_MHL_USE_CODES } from '../components/common/table'; +import { type SelectDataType } from '../commonStore'; +import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; +import { addFileName, flattenFolders, getFileDownload, useCityCd, useDcTree, useSggCd } from '../components/searchModalReportApi'; // import { useBizByFcltStore } from "../../../store/StoreProvider"; 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'; const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; @@ -22,98 +21,7 @@ const TABS = [ { 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_mhl", text: "하수맨홀" }, - { val: "tb_ms_swage_pmstn", text: "하수펌프장" }, - { val: "tb_ms_stofcbr", text: "우수토실" }, -]; -// 운영정보 '구분' 선택 목록 -// const WORK_TYPES = [ -// { val: "maintenance", text: "보수 및 갱신" }, -// { val: "cleaning", text: "청소 및 준설" }, -// { val: "inspection", text: "점검 및 진단" }, -// ]; -const WORK_TYPES = [ - { val: "200", text: "보수 및 갱신(개대체)" }, - { val: "201", text: "보수 및 갱신(유지보수)" }, - { val: "204", text: "보수 및 갱신(단순보수)" }, - { val: "100", text: "점검 및 진단" }, - { val: "203", text: "청소 및 준설" }, -]; -// 운영정보 - 사업 현황 -const operationTableHeadlist = [ - { id: "bizNm", title: "사업명" }, - { id: "flfmtBgngYmd", title: "수행시작일자" }, - { id: "flfmtEndYmd", title: "수행종료일자" }, - { id: "flfmtYr", title: "수행년도" }, - { id: "bizTyCd", title: "사업유형" }, - { id: "bizAmt", title: "사업금액" }, -]; -// 운영정보 - 보수 및 갱신 결과표 -const operationMaintenanceTableHeadlist = [ - { id: "fcltMngNo", title: "관리번호" }, - { id: "usgCd", title: "용도" }, - { id: "mqltCd", title: "재질" }, - { id: "pdmt", title: "관경(mm)" }, - { id: "prlg", title: "연장(m)" }, - { id: "instlYmd", title: "설치일자" }, - // { id: "instlYmd", title: "사업구분" }, -]; -// 운영정보 - 청소 및 준설 결과표 -const operationCleaningTableHeadlist = [ - { id: "fcltMngNo", title: "관리번호" }, - { id: "usgCd", title: "용도" }, - { id: "mqltCd", title: "재질" }, - { id: "pdmt", title: "관경(mm)" }, - { id: "prlg", title: "연장(m)" }, - { id: "instlYmd", title: "설치일자" }, -]; -// 운영정보 - 점검 및 진단 결과표 -const operationInspectionTableHeadlist = [ - { 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: "운영적 상태등급" }, -]; -// 하수도정비 기본계획 보고서 -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: "등록일자" }, -]; -// 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: "0", lwpoCdNm: "전체" }, - { lwpoCd: "SV", lwpoCdNm: "용역" }, - { lwpoCd: "SF", lwpoCdNm: "자체" }, - { lwpoCd: "SC", lwpoCdNm: "공사" }, -]; -const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({ - val: item.lwpoCd, - text: item.lwpoCdNm, -})); // 날짜 조합 함수 const getDateString = ( @@ -151,37 +59,22 @@ const SearchModal: React.FC = ({ const [curPage, setCurPage] = useState(1); const [detailCurPage, setDetailCurPage] = useState(1); const [detailBizId, setDetailBizId] = useState(0); - const [시도, set시도] = useState(0); - const [시군구, set시군구] = useState(0); + const [city, setCity] = useState(0); + const [sgg, setSgg] = useState(0); const [ctpvCd, setCtpvCd] = useState('0'); const [sigungu, setSigungu] = useState('0'); - //const [swageProcessingZone, set처리구역] = useState(""); const [gubun, setGubun] = useState(""); // 하수정비기본계획 report - // 날짜 상태 선언 - 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 [fromDate, setFromDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date }) - const [toDate, setToDate] = useState(new Date()) + 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([{ val: '0', text: '전체', sido: '0' }]); - // const [dynamicHeadlist, setDynamicHeadlist] = useState<{ id: string, title: string }[]>([]); - // const [dynamicHeadlist2, setDynamicHeadlist2] = useState<{ id: string, title: string }[]>([]); // 로딩바 const [isLoading, setIsLoading] = useState(false); @@ -197,22 +90,7 @@ const SearchModal: React.FC = ({ const todayMonth = (today.getMonth() + 1).toString().padStart(2, "0"); const todayDate = today.getDate().toString().padStart(2, "0"); - // const isDateInRanges = (date: Date) => { - // const standYear = parseInt(fromDate.getFullYear().toString()); - // const dateRanges = []; - // 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; - // }); - // }; - - const dynamicHeadlist = useMemo(() => { + 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; @@ -229,7 +107,7 @@ const SearchModal: React.FC = ({ return []; }, [activeTab, basicType, workType]); - const dynamicHeadlist2 = useMemo(() => { + const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => { if (activeTab === 'operation') { if (workType === '200') return operationMaintenanceTableHeadlist; if (workType === '201') return operationMaintenanceTableHeadlist; @@ -243,25 +121,21 @@ const SearchModal: React.FC = ({ useEffect(() => { // filterParams 초기화 - set시작1(""); - set시작2(""); - set종료1(""); - set종료2(""); - set변수1(""); - set변수2(""); - set변수3(""); - set변수4(""); - set가동개시_시작_월(""); - set가동개시_종료_월(""); - set가동개시_시작_일(""); - set가동개시_종료_일(""); - set시도(0); - set시군구(0); + setStart1(""); + setStart2(""); + setEnd1(""); + setEnd2(""); + setVar1(""); + setVar2(""); + setVar3(""); + setVar4(""); + setCity(0); + setSgg(0); setCtpvCd('0'); setSigungu('0'); setYearFilter({ - 시작: "", - 종료: "", + start: "", + end: "", }); setShowTable(false); setTableCount(0); @@ -328,70 +202,24 @@ const SearchModal: React.FC = ({ page: `${curPage - 1}`, size: '5', }; - if (변수1) RequestDTO.bizNm = 변수1; - if (변수2 != '0' && 변수2) RequestDTO.bizTyCd = 변수2; + if (var1) RequestDTO.bizNm = var1; + if (var2 != '0' && var2) RequestDTO.bizTyCd = var2; // 수행기간 범위 const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${todayYear}${todayMonth}${todayDate}`, ); RequestDTO.fromflfmtEndYmd = startDate; RequestDTO.toflfmtEndYmd = endDate; - // let allContent: any[] = []; - // let totalCount = 0; - - // if (workType === 'maintenance') { - // const codes = ['200', '201', '204']; - // for (const code of codes) { - // RequestDTO.bizSeCd = code; - // 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) => { - // const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTyCd); - // return { - // ...item, - // bizTyCd: matched ? matched.lwpoCdNm : item.bizTyCd, - // }; - // }); - // allContent = allContent.concat(mappedContent); - // totalCount += res.totalElements || 0; - // } - // setTableData(allContent); - // setTableCount(totalCount); - // } else { - // if (workType === 'cleaning') { - // RequestDTO.bizSeCd = '203'; - // } else if (workType === 'inspection') { - // RequestDTO.bizSeCd = '100'; - // } - // 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) => { - // const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTyCd); - // return { - // ...item, - // bizTyCd: matched ? matched.lwpoCdNm : item.bizTyCd, - // }; - // }); - // setTableData(mappedContent || []); - // setTableCount(res.totalElements || 0); - // } - RequestDTO.bizSeCd = workType; @@ -427,13 +255,13 @@ const SearchModal: React.FC = ({ const sigunguOption = filteredSiguns.find((n) => n.val === sigungu); filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text); } - if (yearFilter.시작 != "" && yearFilter.종료 == "") //승인년도검색 - filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.시작 + "년" : null); - if (yearFilter.시작 == "" && yearFilter.종료 != "") - filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.종료 + "년" : null); - if (yearFilter.시작 != "" && yearFilter.종료 != "") { + 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.시작 + "년" && item.approvalYear <= yearFilter.종료 + "년" : null; + return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null; }); } if (gubun !== "") { // 구분검색 @@ -464,33 +292,33 @@ const SearchModal: React.FC = ({ const pplCqlFilters = () => { const cqlFilters: string[] = []; // 추후 작성 예정 - if (변수1) cqlFilters.push(`usg_cd='${변수1}'`); - if (변수2) cqlFilters.push(`mqlt_cd='${변수2}'`); - if (변수3) cqlFilters.push(`pbadms_emd_cd='${변수3}'`); + 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.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${todayYear}${todayMonth}${todayDate}`, ); cqlFilters.push( `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, ); // 관경(mm) 범위 - if (시작1 && 종료1) cqlFilters.push(`pdmt BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`pdmt >= ${시작1}`); - else if (종료1) cqlFilters.push(`pdmt <= ${종료1}`); + 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 (시작2 && 종료2) cqlFilters.push(`prlg BETWEEN ${시작2} AND ${종료2}`); - else if (시작2) cqlFilters.push(`prlg >= ${시작2}`); - else if (종료2) cqlFilters.push(`prlg <= ${종료2}`); + 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; }; @@ -499,41 +327,41 @@ const SearchModal: React.FC = ({ const cqlFilters: string[] = []; // 가동개시일자 범위 const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${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 (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 (시작2 && 종료2) - cqlFilters.push(`prcsZoneAreaSum BETWEEN ${시작2} AND ${종료2}`); - else if (시작2) cqlFilters.push(`prcsZoneAreaSum >= ${시작2}`); - else if (종료2) cqlFilters.push(`prcsZoneAreaSum <= ${종료2}`); + 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 (변수1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${변수1}%'`); + if (var1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var1}%'`); 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}%'`); + 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 필터 생성 @@ -541,25 +369,25 @@ const SearchModal: React.FC = ({ const cqlFilters: string[] = []; // // 설치년도 범위 const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${todayYear}${todayMonth}${todayDate}`, ); cqlFilters.push( `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, ); - if (변수1) cqlFilters.push(`usg_cd='${변수1}'`); + if (var1) cqlFilters.push(`usg_cd='${var1}'`); // 구경(mm) 범위 - if (시작1 && 종료1) cqlFilters.push(`swage_mh_diam BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`swage_mh_diam >= ${시작1}`); - else if (종료1) cqlFilters.push(`swage_mh_diam <= ${종료1}`); + 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; }; @@ -568,30 +396,30 @@ const SearchModal: React.FC = ({ const cqlFilters: string[] = []; // 설치년도 범위 const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${todayYear}${todayMonth}${todayDate}`, ); cqlFilters.push( `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, ); // 일일처리용량 범위 - if (시작1 && 종료1) - cqlFilters.push(`daily_prcs_cpct BETWEEN ${시작1} AND ${종료1}`); - else if (시작1) cqlFilters.push(`daily_prcs_cpct >= ${시작1}`); - else if (종료1) cqlFilters.push(`daily_prcs_cpct <= ${종료1}`); + 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 (시작2 && 종료2) - cqlFilters.push(`excln_wpmp_ablt BETWEEN ${시작2} AND ${종료2}`); - else if (시작2) cqlFilters.push(`excln_wpmp_ablt >= ${시작2}`); - else if (종료2) cqlFilters.push(`excln_wpmp_ablt <= ${종료2}`); + 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; }; @@ -600,15 +428,15 @@ const SearchModal: React.FC = ({ const cqlFilters: string[] = []; // 설치년도 범위 const startDate = getDateString( - yearFilter.시작, - 가동개시_시작_월, - 가동개시_시작_일, + yearFilter.start, + '', + '', "19000101", ); const endDate = getDateString( - yearFilter.종료, - 가동개시_종료_월, - 가동개시_종료_일, + yearFilter.end, + '', + '', `${todayYear}${todayMonth}${todayDate}`, ); cqlFilters.push( @@ -725,7 +553,7 @@ const SearchModal: React.FC = ({ } setSigungu("0"); setGubun(""); - setYearFilter({ 시작: "", 종료: "", }); + setYearFilter({ start: "", end: "", }); } const renderBody = () => ( @@ -742,490 +570,62 @@ const SearchModal: React.FC = ({ {/* 각각의 탭 영역 */} {/* 기본정보 탭 */} {activeTab === 'basic' && ( -
- {/* 구분 */} -
-
- - setBasicType(String(value)) - } - value={basicType} - /> -
-
- {/* 분기처리: 하수관로 */} - {basicType === "tb_ms_swage_ppl" && ( -
-
- {/* 설치년도 */} - - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="설치년도" - endLabel="" - /> - {/* 용도 */} - { - set변수1(String(val)); - }} - /> - {/* 재질 */} - { - set변수2(String(val)); - }} - /> - {/* 관경 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 연장 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작2(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료2(val); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
- )} - {/* 분기처리: 하수처리시설 */} - {basicType === "tb_il_swage_prcs_fclt" && ( -
- {/* 가동개시일자 */} -
- - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="가동개시일자" - endLabel="" - /> - {/* 시설용량 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 처리구역 면적 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작2(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료2(val); - }} - /> - {/* 하수처리시설명 */} - - { - set변수1(e.target.value); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
- )} - {/* 분기처리: 하수처리구역 */} - {basicType === "tb_ms_swage_prcs_zone" && ( -
-
- { - set시도(sidoId); - set시군구(0); - }} - onSigunguChange={set시군구} - /> - - { - set변수1(e.target.value); - }} - /> - - { - set변수2(e.target.value); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
- )} - {/* 분기처리: 하수맨홀 */} - {basicType === "tb_ms_swage_mhl" && ( -
-
- {/* 설치년도 */} - - setYearFilter({ - ...yearFilter, - 시작: range.start, - 종료: range.end, - }) - } - startLabel="설치년도" - endLabel="" - /> - {/* 용도 */} - { - set변수1(String(val)); - }} - /> - {/* 하수맨홀 구경 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
- )} - {/* 분기처리: 하수펌프장 */} - {basicType === "tb_ms_swage_pmstn" && ( -
- {/* 설치년도 */} -
- setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="설치년도" - endLabel="" - /> - {/* 일일처리용량 - 오수맨홀 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작1(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료1(val); - }} - /> - {/* 양수능력 - 우수맨홀 */} - - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set시작2(val); - }} - /> - ~ - { - // 숫자만 입력 허용 - const val = e.target.value.replace(/[^0-9]/g, ""); - set종료2(val); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
- )} - {/* 분기처리: 우수토실? 토구? */} - {basicType === "tb_ms_stofcbr" && ( -
- {/* 설치년도 */} -
- setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="설치년도" - endLabel="" - /> - {/* 조회 버튼 */} -
- -
-
-
- )} -
+ )} {/* 운영정보 탭 */} {activeTab === 'operation' && ( -
-
- ({ - val: w.val, - text: w.text, - }))} - onSelectValue={(val) => setWorkType(String(val))} - value={workType} - /> -
-
- - { - set변수1(e.target.value); - }} - /> - setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="수행기간" - endLabel="" - /> - { - set변수2(String(val)); - }} - /> - {/* 조회 버튼 */} -
- -
-
-
+ )} {/* 하수도정비기본계획보고서 탭 */} {activeTab === 'report' && ( -
- {/* 시도, 시군구, 하수처리구역 한 줄 */} -
- - setSigungu(String(val))} - /> -
-
- - { - // 숫자만 입력 허용 - const val = e.target.value; - setGubun(val); - }} - /> - setYearFilter({ ...yearFilter, 시작: range.start, 종료: range.end })} - startLabel="승인년도" - endLabel="" - /> - {/* 조회 버튼 */} -
- -
-
-
+ )} {/* */} @@ -1282,10 +682,11 @@ const SearchModal: React.FC = ({
{ - const label = WORK_TYPES.find((type) => type.val === workType)?.text - return (<>{label} 결과표 ({detailTableCount})건); - })() + headName={ + (() => { + const label = WORK_TYPES.find((type) => type.val === workType)?.text + return (<>{label} 결과표 ({detailTableCount})건); + })() } tableData={detailTableData} headlist={dynamicHeadlist2} @@ -1309,9 +710,9 @@ const SearchModal: React.FC = ({ bodyClass={` h-[100vh] w-[60vw] - max-w-none - max-h-none - overflow-visible + max-w-[60vw] + max-h-[100vh] + overflow-hidden z-[101] `} body={renderBody()} 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 = ({ + yearFilter, + setYearFilter, + handleSearch, +}) => { + return ( +
+ {/* 설치년도 */} +
+ setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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..9c2ea9594 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx @@ -0,0 +1,94 @@ +// 하수맨홀 + +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 = ({ + var1, + start1, + end1, + yearFilter, + setVar1, + setStart1, + setEnd1, + setYearFilter, + handleSearch, +}) => { + return ( +
+
+ {/* 설치년도 */} + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 하수맨홀 구경 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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..ff023b996 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx @@ -0,0 +1,102 @@ +// 하수펌프장 + +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 = ({ + start1, + start2, + end1, + end2, + yearFilter, + setStart1, + setStart2, + setEnd1, + setEnd2, + setYearFilter, + handleSearch, +}) => { + return ( +
+
+ {/* 설치년도 */} + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 일일처리용량 - 오수맨홀 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + /> + {/* 양수능력 - 우수맨홀 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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..03787cdc7 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx @@ -0,0 +1,132 @@ +// 하수관로 + +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 = ({ + var1, + var2, + start1, + end1, + yearFilter, + setVar1, + setVar2, + setStart1, + setEnd1, + setYearFilter, + handleSearch, +}) => { + return ( +
+
+ {/* 설치년도 */} + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 재질 */} + { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 관경 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setVar1(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + /> + {/* 연장 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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..0ea3ea53a --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx @@ -0,0 +1,121 @@ +// 하수처리시설 + +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 = ({ + var1, + start1, + start2, + end1, + end2, + yearFilter, + setVar1, + setStart1, + setStart2, + setEnd1, + setEnd2, + setYearFilter, + handleSearch, +}) => { + return ( +
+ {/* 가동개시일자 */} +
+ + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="" + endLabel="" + /> + {/* 시설용량 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + /> + {/* 처리구역 면적 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + /> + {/* 하수처리시설명 */} + + { + setVar1(e.target.value); + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +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..98bc76b39 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx @@ -0,0 +1,69 @@ +// 하수처리구역 + +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 = ({ + var1, + var2, + city, + sgg, + setVar1, + setVar2, + setCity, + setSgg, + handleSearch, +}) => { + return ( +
+
+ { + setCity(sidoId); + setSgg(0); + }} + onSigunguChange={setSgg} + /> + + { + setVar1(e.target.value); + }} + /> + + { + setVar2(e.target.value); + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(swagePrcsZone); \ 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 index a1a0e701e..d2d7d2672 100644 --- a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -4,7 +4,6 @@ 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 { SelectBox2 } from '@common/business_components/ui/select/index-2' import { Button, Label } from '@common/components/ui' import { useEffect, useState, type ReactNode } from 'react' import LoadingOverlay from '@/components/ui/LoadingBar' @@ -259,8 +258,8 @@ const BaseLayout = ({ setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 > - onSwageTreatmentPlantChange?.(val)} /> - {/* onSwageProcessingZoneChange?.(val)} /> */} + onSwageTreatmentPlantChange?.(val)} selectContentClass="z-[101]" /> + {/* onSwageProcessingZoneChange?.(val)} selectContentClass="z-[101]" /> */} {selectBox?.searchChk ? : null}
diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 8d8b8def7..0c0c88973 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -5,11 +5,29 @@ const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; export const getData = async (url: string, data?: object): Promise => { const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { method: "GET", - body: JSON.stringify(data), }); if (res.ok) { - const data = await res.json(); - return data; + 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'); }; @@ -93,7 +111,7 @@ export const getCmsData = async (url: string, data?: object): Promise => { throw new Error('Failed to fetch data'); }; -export function getCookie(name:string) { +export function getCookie(name: string) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); if (match && match[2]) { return decodeURIComponent(match[2]); 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 (
- ({ val: opt.id, text: opt.name }))} value={sido} onSelectValue={val => onSidoChange(Number(val))} + selectContentClass="z-[101]" /> - ({ val: opt.id, text: opt.name }))} value={sigungu} onSelectValue={val => onSigunguChange(Number(val))} + selectContentClass="z-[101]" />
); diff --git a/apps/sn/app/components/ui/YearRangeSelectBox.tsx b/apps/sn/app/components/ui/YearRangeSelectBox.tsx index 768bfd54c..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 (
- onChange({ start: String(val), end })} value={start} + selectContentClass="z-[101]" /> ~ - onChange({ start, end: String(val) })} value={end} + selectContentClass="z-[101]" />
); diff --git a/package.json b/package.json index 9e318b357..f3627edb6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "preinstall": "npx only-allow pnpm" }, "dependencies": { + "chart.js": "^4.4.8", "react": "^18.3.1", + "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", "zustand": "^5.0.3" }, diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 8a2db3ae2..918585c25 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -1,11 +1,11 @@ import { MultiBackend, Tree, - getBackendOptions + getBackendOptions, + DndProvider } from "@minoru/react-dnd-treeview"; import { ChevronLeft, ChevronRight } from 'lucide-react'; import React, { useEffect, useState } from 'react'; -import { DndProvider } from "react-dnd"; import { useGisContext } from './Gis'; import GisApp from './lib/gis.core'; 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 1a6b74c96..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 @@ -42,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'; 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 7790aa5dc..f0e29d31c 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,7 +71,7 @@ GisOlLayer.prototype = { /** * [사용]WFS 벡터 소스 반환 */ - getVectorSourceGeoserver: function (options) { + getVectorSourceBbox: function (options) { let url = GisApp.GEOSERVER_URL; let toProj = GisApp.TO_PROJ; let lyrTyNm = options.lyrTyNm; @@ -110,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'; @@ -119,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; }, @@ -142,8 +140,9 @@ GisOlLayer.prototype = { option = option || {}; //레이어 생성 let returnLayer = this.makeLayer(option); + //레이어 소스 생성 - let source = this.getVectorSourceGeoserver(option); + let source = this.getVectorSourceBbox(option); returnLayer.setSource(source); //스타일 추가 returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, option)); diff --git a/packages/common/components/package.json b/packages/common/components/package.json index 0edd7211b..96d381605 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -57,6 +57,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "dayjs": "^1.11.13", "embla-carousel-react": "^8.6.0", "js-cookie": "^3.0.5", "lucide-react": "^0.474.0", diff --git a/packages/common/components/src/api/menu.ts b/packages/common/components/src/api/menu.ts index 8d58a3499..82d48815f 100644 --- a/packages/common/components/src/api/menu.ts +++ b/packages/common/components/src/api/menu.ts @@ -1,5 +1,6 @@ import { getApiClient } from "./client"; import { ENDPOINT } from "../config"; +import { getConfig } from "@common/config"; export interface MenuItem { menuCd: number; @@ -26,6 +27,8 @@ export interface TreeMenu extends MenuItem { } const client = getApiClient(); +const { PROXY_HOST } = getConfig(); +const BASE_URL = `${PROXY_HOST}/server`; export const getMenuList = async (siteId: string) => { const response = await client.get( @@ -40,3 +43,18 @@ export const getPreviewTreeMenuList = async (siteId: string) => { ); return response.data; }; + +export const getAnonymouseMenuList = async ( + siteId: string, +): Promise => { + const response = await fetch( + `${BASE_URL}/${ENDPOINT.CMS_SERVICE.MENU_ANONYMOUS}/user/${siteId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + return response.json(); +}; diff --git a/packages/common/components/src/api/site.ts b/packages/common/components/src/api/site.ts index 8d8109571..da4de293f 100644 --- a/packages/common/components/src/api/site.ts +++ b/packages/common/components/src/api/site.ts @@ -1,4 +1,4 @@ -import { getApiClient } from "./client"; +import { getConfig } from "@common/config"; import { ENDPOINT } from "../config"; export interface SiteInfo { @@ -23,11 +23,18 @@ export interface SiteInfo { mdfrNm: string; } -const client = getApiClient(); +const { PROXY_HOST } = getConfig(); +const BASE_URL = `${PROXY_HOST}/server`; export const getSiteInfo = async (siteId: string) => { - const response = await client.get( - `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, + const response = await fetch( + `${BASE_URL}/${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, ); - return response.data; + return response.json(); }; diff --git a/packages/common/components/src/config/api.ts b/packages/common/components/src/config/api.ts index 0ead6b0bc..6772182fb 100644 --- a/packages/common/components/src/config/api.ts +++ b/packages/common/components/src/config/api.ts @@ -2,6 +2,7 @@ export const ENDPOINT = { CMS_SERVICE: { MENUS: "/cms-service/api/v1/menus", MENU_ROLES: "/cms-service/api/v1/menu-roles", + MENU_ANONYMOUS: "/cms-service/api/v1/menu/anonymous", SITES: "/cms-service/api/v1/sites", POPUPS: "/cms-service/api/v1/popups", IPS: "/cms-service/api/v1/site-ips", diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 433435009..f7637b7f0 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -3,6 +3,7 @@ import "./header.css"; import { useEffect, useCallback, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { images } from "@common/assets"; +import { getConfig } from "@common/config"; import { ENDPOINT } from "../../config"; import { setCookie, getCookie } from "../../lib"; import { @@ -22,7 +23,10 @@ import { Button } from "../button"; import { SidebarTrigger } from "../sidebar"; import { Skeleton } from "../skeleton"; -const siteId = getCookie("site-id-key") ?? ""; +const appConfig = getConfig(); +const siteId = appConfig.SITE_ID ?? ""; +const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); +const isLoggedIn = !!userId; export function Header({ viewOnly }: { viewOnly?: boolean }) { const [mounted, setMounted] = useState(false); @@ -30,7 +34,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { const { data: userInfo } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS], queryFn: () => getUserInfo(), - enabled: !viewOnly, + enabled: isLoggedIn && !viewOnly, }); const { data: siteInfo } = useQuery({ @@ -50,6 +54,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { const { data: userPopupList } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], queryFn: () => getUserPopupList(), + enabled: isLoggedIn && !viewOnly, }); const logout = async () => { @@ -130,30 +135,36 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
- - - - - - 프로필 설정 -
-

최종접속일

-

{userInfo?.lastLoginDate}

-
-
-

접속 IP

-

-
- 로그아웃 -
-
+ {mounted ? ( + isLoggedIn && ( + + + + + + 프로필 설정 +
+

최종접속일

+

{userInfo?.lastLoginDate}

+
+
+

접속 IP

+

+
+ 로그아웃 +
+
+ ) + ) : ( + + )} ); } diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 75231add8..ae913e79a 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -7,6 +7,7 @@ export * from "./carousel"; export * from "./checkbox"; export * from "./data-table"; export * from "./date-picker"; +export * from "./input-date-picker"; export * from "./date-range-picker"; export * from "./dialog"; export * from "./dropdown-menu"; diff --git a/packages/common/components/src/ui/input-date-picker/index.tsx b/packages/common/components/src/ui/input-date-picker/index.tsx new file mode 100644 index 000000000..93d2caf7c --- /dev/null +++ b/packages/common/components/src/ui/input-date-picker/index.tsx @@ -0,0 +1,136 @@ +"use client"; + +import dayjs from "dayjs"; +import { useEffect, useState } from "react"; +import { CalendarIcon } from "lucide-react"; + +import { Button } from "../button"; +import { Calendar } from "../calendar"; +import { Input } from "../input"; +import { Popover, PopoverContent, PopoverTrigger } from "../popover"; + +import "./input-date-picker.css"; + +export interface InputDatePickerProps { + value?: Date; + defaultValue?: Date; + onChange?: (date: Date | undefined) => void; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +export function InputDatePicker({ + value: controlledValue, + defaultValue, + onChange, + placeholder, + disabled, + className, +}: InputDatePickerProps) { + const isControlled = controlledValue !== undefined; + + const [open, setOpen] = useState(false); + const [internalDate, setInternalDate] = useState( + defaultValue, + ); + const [month, setMonth] = useState( + isControlled ? controlledValue : defaultValue, + ); + + // 실제로 사용할 date 값 (제어/비제어 모두 지원) + const date = isControlled ? controlledValue : internalDate; + + // 표시할 문자열 값 + const [displayValue, setDisplayValue] = useState(date); + + // 제어 컴포넌트일 경우 외부 값 변경 감지 + useEffect(() => { + if (isControlled) { + setDisplayValue(controlledValue); + setMonth(controlledValue); + } + }, [isControlled, controlledValue]); + + // 내부 상태 변경 핸들러 + const handleDateChange = (newDate: Date | undefined) => { + if (!isControlled) { + setInternalDate(newDate); + } + + if (onChange) { + onChange(newDate); + } + + setDisplayValue(newDate); + }; + + return ( +
+
+ { + const newDate = new Date(e.target.value); + setDisplayValue(newDate); + + if (isValidDate(newDate)) { + handleDateChange(newDate); + setMonth(newDate); + } + }} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setOpen(true); + } + }} + placeholder={placeholder} + /> + + + + + + { + handleDateChange(newDate); + setOpen(false); + }} + /> + + +
+
+ ); +} + +function isValidDate(date: Date | undefined) { + if (!date) { + return false; + } + return !isNaN(date.getTime()); +} diff --git a/packages/common/components/src/ui/input-date-picker/input-date-picker.css b/packages/common/components/src/ui/input-date-picker/input-date-picker.css new file mode 100644 index 000000000..1039a6a63 --- /dev/null +++ b/packages/common/components/src/ui/input-date-picker/input-date-picker.css @@ -0,0 +1,7 @@ +@tailwind components; + +@layer components { + .input-date-picker::-webkit-calendar-picker-indicator { + display: none; + } +} diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index 650fd2474..8f63a08f3 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { ChevronRight } from "lucide-react"; +import { getConfig } from "@common/config"; import { getParentIds } from "./lib"; import { Sidebar, @@ -22,7 +23,17 @@ import { } from "../collapsible"; import { ENDPOINT } from "../../config"; import { cn, getCookie, setCookie } from "../../lib"; -import { getMenuList, getPreviewTreeMenuList, type MenuItem } from "../../api"; +import { + getAnonymouseMenuList, + getMenuList, + getPreviewTreeMenuList, + type MenuItem, +} from "../../api"; + +const appConfig = getConfig(); +const appSiteId = appConfig.SITE_ID ?? ""; +const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); +const isLoggedIn = !!userId; export function LNB({ viewOnly, @@ -42,24 +53,34 @@ export function LNB({ handleMenuClick?: (item: MenuItem) => void; }) { const [defaultOpenMenuIds, setDefaultOpenMenuIds] = useState([]); + const [currentMenuCode, setCurrentMenuCode] = useState(null); const prevPathRef = useRef(currentPath); - const cookieSiteId = getCookie("site-id-key") ?? ""; - const siteId = propSiteId ?? cookieSiteId; + const siteId = propSiteId ?? appSiteId; const { data: menuListData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], queryFn: () => getMenuList(siteId), - enabled: !viewOnly && !previewMode, + enabled: isLoggedIn && !viewOnly && !previewMode, }); const { data: previewTreeMenuListData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId, "preview"], queryFn: () => getPreviewTreeMenuList(siteId), - enabled: previewMode, + enabled: isLoggedIn && previewMode, }); - const menuList = previewMode ? previewTreeMenuListData : menuListData; + const { data: anonymousMenuListData } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_ANONYMOUS, siteId], + queryFn: () => getAnonymouseMenuList(siteId), + enabled: !isLoggedIn, + }); + + const menuList = isLoggedIn + ? previewMode + ? previewTreeMenuListData + : menuListData + : anonymousMenuListData; function isPathMatched(path: string) { if (!path) return false; @@ -70,6 +91,7 @@ export function LNB({ if (!menuList || !currentPath) return; const matchedMenuIds: number[] = []; + let matchedMenuCode: number | null = null; const findMatchedMenus = (items: MenuItem[]) => { items.forEach((item) => { @@ -77,25 +99,32 @@ export function LNB({ const parentIds = getParentIds(item.menuCd)(menuList); matchedMenuIds.push(...parentIds, item.menuCd); } + if (item.children) findMatchedMenus(item.children); + }); + }; - if (item.children) { - findMatchedMenus(item.children); - } + const findMatchedMenuCode = (items: MenuItem[]) => { + items.forEach((item) => { + if (matchedMenuCode) return; + if (currentPath === item.urlPath) matchedMenuCode = item.menuCd; + if (item.children) findMatchedMenuCode(item.children); }); }; findMatchedMenus(menuList); + findMatchedMenuCode(menuList); - if (matchedMenuIds.length > 0) { + if (matchedMenuIds.length > 0) setDefaultOpenMenuIds((prev) => [ ...new Set([...prev, ...matchedMenuIds]), ]); - } if (prevPathRef.current !== currentPath) { setDefaultOpenMenuIds(matchedMenuIds); prevPathRef.current = currentPath; } + + if (matchedMenuCode) setCurrentMenuCode(matchedMenuCode); }, [menuList, currentPath]); function MenuItem({ @@ -106,7 +135,8 @@ export function LNB({ isSubMenu?: boolean; }) { const { open: isSidebarOpen, setOpen } = useSidebar(); - const isMatched = isPathMatched(item.urlPath); + const isMatched = + isPathMatched(item.urlPath) && currentMenuCode === item.menuCd; const handleClick = () => { if (!isSidebarOpen) setOpen(true);