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/popupComponent/chart/aplcnYms.ts b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts index 81daed0ed..99a563184 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts @@ -1,6 +1,7 @@ import {useState, useEffect} from 'react'; import { getData, getDcData } from '@/components/lib/api'; import moment from "moment"; +import { library } from 'webpack'; export type TInftwtAnls = { inftwtAnlsSn:number; @@ -64,7 +65,20 @@ export type TOilqData = { mtrngDt: string; // 계측일시 oilq: number; // 유량 } - +export type TRainData = { + rainInfwtAnlsSn: number; + mtrngDt: string; + amrf: number; // 강우량 +} +type lineBarChartDataProps = { + label : string; + lineVal : number; + barVal : number; + barVal0 : number; + barVal1 : number; + lineVal0 : number; + lineVal1 : number; +} const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => { const yyyymms1 = yyyymms.map(dateStr => { const year = dateStr.slice(0, 4); @@ -223,10 +237,11 @@ export const selectRdiiAnls = async(aplcnYm:String) => { // 침입수분석 console.error(error); } }; -export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { +export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => { const options = arr.map(anlsRslt => ({ label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), - val: anlsRslt.predcOilq, + lineVal0: anlsRslt.predcOilq, + barVal0: 0 })); return options; }; @@ -237,7 +252,7 @@ export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: str })); return options; }; -export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과 +export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과 try { if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) { console.warn("RdiiAnlsSn or pplMngNo is not provided"); @@ -245,10 +260,32 @@ export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) } const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`); if (result && result.numberOfElements > 0) { - console.log("selectRdiiAnlsRslt", result.content); return result.content; } } catch (error) { console.error(error); } }; +export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) { + console.warn("RdiiAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`); + if (result && result.numberOfElements > 0) { + console.log("selectRainData", result.content); + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => { + const options = arr.map(data => ({ + label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"), + lineVal0: 0, + barVal0: data.amrf, + })); + return options; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx index 9735c772f..c349d08cc 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -11,7 +11,13 @@ import { useRdiiChartModalStore } from '../chartModalStore'; // 모달 상태 type ModalOpenProp = { pplMngNo: string, // 관로관리번호 } -type DataType = {label: string; val: number}; +// 차트 +type lineBarChartData = { + label : string; + lineVal0 : number; + barVal0 : number; +} + const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); // 모달 상태 관리 스토어 @@ -23,10 +29,9 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState([{ val: 0, text: '선택' }]); // 강우유입수분석정보 options const [rdiiAnls, setRdiiAnls] = useState(null); // 강우유입수분석정보 const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState(0); // 강우유입수분석 정보 선택시 사용 - const [chartData, setChartData] = useState<{ chartData1: DataType[]; }>({ - chartData1: [], //chartData2: [], - }); + const [chartData, setChartData] = useState([]); const [rdiiQty, setRdiiQty] = useState(0); // 강우유입수량 + const [rainTotal, setRainTotal] = useState(0); // 적용강우량 const [totalVolume, setTotalVolume] = useState(0); // 총체적 m³/mm const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기 pplMngNo = "L"+pplMngNo; // "L13101003"; @@ -42,10 +47,16 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { setPopupOpen(true); // 팝업 열기 }; - const chartForm = CHART_FORM.LINE; + const chartForm = CHART_FORM.LINEBAR; const chartConfig = { - chartData1: { label: 'RDII', color: "red", }, - //chartData2: { label: '순오수량(m³/h)', color: "orange", }, + barVal0: { + label: '강수량', + color: "#33AAFF", + }, + lineVal0: { + label: 'RDII', + color: "#FF0000", + }, } as ChartConfig; useEffect(() => { @@ -56,8 +67,9 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { setRdiiAnlsInfos([{ val: 0, text: '선택' }]); // 강우유입수분석정보 초기화 setRdiiAnls(null); // 강우유입수분석정보 초기화 setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화 - setChartData({chartData1: [], }); // 차트 데이터 초기화 + setChartData([]); // 차트 데이터 초기화 setRdiiQty(0); // 강우유입수량 초기화 + setRainTotal(0); // 적용강우량 초기화 setTotalVolume(0); // 총체적 m³/mm 초기화 } }, [isModalOpen]); @@ -99,23 +111,34 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { setPopupOpen(false); } const rdiiRsltChart = () => { // 그래프 & 팝업 닫기 - fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수분석 결과 조회 - const rsltData1 = fn.getChartDataRdiiAnlsRslt(data ?? []); - //const rsltData2 = fn.getChartDataRdiiAnlsRsltMinq(data ?? []); - setChartData(prev => ({ - chartData1: rsltData1, // 순오수량(m³/h) - // chartData2: rsltData2 // RDII(m³/h) 강우유입수분석결과 - })); - }); + fetchFilteredData(); fn.selectPplRdiiQty(rainInfwtAnlsSn, pplMngNo).then((data) => {// 강우유입수량 조회 setRdiiQty(Math.round(data.rdiih*24)); // cmd: m³/d 단위로 변환 if (data.raintotal && data.rainTotal !== 0) { + setRainTotal(data.raintotal); // 총강우량mm setTotalVolume(Math.round(data.rdiih * data.sustaintime / data.raintotal)); // 총체적 m³/mm } }); setPopupOpen(false); }; + const fetchFilteredData: () => Promise = async () => { + const rsltData = await fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, pplMngNo); + const rsltDataformatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []); // format 변경 + const rainData = await fn.selectRainData(rainInfwtAnlsSn); + const rainDataformatted = fn.getChartDataRainData(rainData ?? []); // format 변경 + console.log('rainDataformatted :', rainDataformatted); + const rainMap = new Map(rainDataformatted.map(item => [item.label, item.barVal0])); + + rsltDataformatted.forEach(item => { + const matchedVal = rainMap.get(item.label); + if (matchedVal !== undefined) { + item.barVal0 = matchedVal; + } + }); + console.log('rsltDataformatted :', rsltDataformatted); + setChartData(rsltDataformatted); // 차트 데이터 설정 + } const renderBody = () => ( <>
RDII분석결과
@@ -143,6 +166,11 @@ const RdiiChart = ({ pplMngNo }: ModalOpenProp) => { {rdiiQty} m³/d
+
+ 적용강우량 : + {rainTotal} + mm +
강우량대비RDII : {totalVolume} 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);