Merge remote-tracking branch 'origin/main' into iipbs/sn/dev

This commit is contained in:
yschoi
2025-07-24 20:51:46 +09:00
28 changed files with 12033 additions and 164 deletions
+33 -21
View File
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { import {
Button, Button,
DateRangePicker,
Input, Input,
InputDatePicker,
Label, Label,
Select, Select,
SelectContent, SelectContent,
@@ -21,7 +21,7 @@ import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/apilog/model/table-columns"; import { columns } from "@/pages/apilog/model/table-columns";
type DateRange = { type DateRange = {
from: Date | undefined; from?: Date | undefined;
to?: Date | undefined; to?: Date | undefined;
}; };
@@ -35,6 +35,7 @@ function ApiLogPage() {
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}); });
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange); const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState(""); const [userId, setUserId] = useState("");
const [apiHttpPmhd, setApiHttpPmhd] = useState(""); const [apiHttpPmhd, setApiHttpPmhd] = useState("");
@@ -84,8 +85,10 @@ function ApiLogPage() {
...searchParams, ...searchParams,
userId, userId,
apiHttpPmhd, apiHttpPmhd,
dmndDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"), dmndDtStart: dateRange.from
dmndDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"), ? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
dmndDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
}); });
}; };
@@ -126,6 +129,7 @@ function ApiLogPage() {
default: default:
presetDateRange = undefined; presetDateRange = undefined;
} }
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange); setDateRange(presetDateRange || defaultDateRange);
}; };
@@ -134,23 +138,31 @@ function ApiLogPage() {
<div className="card card-border"> <div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label> <Label className="mr-2">조회기간</Label>
<DateRangePicker <Select value={presetDateRange} onValueChange={handlePresetDateRange}>
value={dateRange} <SelectTrigger className="w-[12rem]">
onValueChange={(value) => setDateRange(value || defaultDateRange)} <SelectValue placeholder="기간 선택" className="w-[24rem]" />
> </SelectTrigger>
<div className="card card-border flex items-center gap-2"> <SelectContent>
<Select onValueChange={handlePresetDateRange}> <SelectItem value="1W">일주일</SelectItem>
<SelectTrigger> <SelectItem value="1M">1개월</SelectItem>
<SelectValue placeholder="기간 선택" className="w-[24rem]" /> <SelectItem value="3M">3개월</SelectItem>
</SelectTrigger> </SelectContent>
<SelectContent> </Select>
<SelectItem value="1W">일주일</SelectItem> <InputDatePicker
<SelectItem value="1M">1개월</SelectItem> value={dateRange.from}
<SelectItem value="3M">3개월</SelectItem> onChange={(date) => {
</SelectContent> setDateRange({ ...dateRange, from: date });
</Select> setPresetDateRange("");
</div> }}
</DateRangePicker> />
~
<InputDatePicker
value={dateRange.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label> <Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input <Input
value={userId} value={userId}
@@ -0,0 +1,14 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { Dashboard, DashboardService } from "../model/dashboard-interface";
export class DashboardApiService implements DashboardService {
public async getDashboard(): Promise<Dashboard> {
const response = await client.get<Dashboard>(
ENDPOINT.CMS_SERVICE.DASHBOARD,
);
return response.data;
}
}
export const DashboardApi = new DashboardApiService();
+49
View File
@@ -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<string, ChartDataset> = 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<string, ChartDataset>,
);
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"];
}
@@ -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,
},
},
},
},
};
@@ -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<Dashboard>;
}
@@ -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<RecentPost>[] = [
{
header: "번호",
cell: ({ row, table }) => {
if (row.original.upndFixYn === "Y") {
return (
<Badge
color="orange"
className="flex size-8 items-center justify-center"
>
<i className="diveicon di-volume-up-fill" />
</Badge>
);
}
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<RecentPost>[] = [
{
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 };
@@ -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() { 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 (
<div className="p-6">
<div className="flex flex-col gap-1">
{/* 첫번째 라인 */}
<div className="grid grid-cols-4 gap-1">
{/* 총 회원수 카드 */}
<div className="card card-border h-80 !bg-primary !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter text-white">
<h3 className="text-2xl font-medium">총 회원수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.memberTotCnt ?? "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
금일 신규 가입자 {data?.memberTodayCnt ?? "-"}명
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.memberChartData!.data as ChartData<"line">}
options={chartData.memberChartData!.options}
/>
</div>
</div>
</div>
{/* 전체 게시물 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">전체 게시물</h3>
<div className="mt-2 text-6xl font-bold">
{data?.pstTotCnt ?? "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
금일 +{data?.pstTodayCnt ?? "-"} 등록
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.postChartData!.data as ChartData<"line">}
options={chartData.postChartData!.options}
/>
</div>
</div>
</div>
{/* 오늘 접속자 수 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">오늘 접속자 수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.accessTodayCnt ?? "-"}
</div>
<p className="mt-1 text-2xl opacity-80">
전일 대비 +{data?.accessBefDayPer ?? "-"}%
</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.visitorChartData!.data as ChartData<"line">}
options={chartData.visitorChartData!.options}
/>
</div>
</div>
</div>
{/* API 호출 건수 카드 */}
<div className="card card-border h-80 !p-0">
<div className="relative h-full">
<div className="absolute left-0 top-0 p-6 tracking-tighter">
<h3 className="text-2xl font-medium">API 호출 건수</h3>
<div className="mt-2 text-6xl font-bold">
{data?.apiTodayCnt ?? "-"}
</div>
<p className="mt-1 text-2xl opacity-80">금일 누적</p>
</div>
{/* 차트 영역 */}
<div className="h-full">
<Line
data={chartData.apiChartData!.data as ChartData<"line">}
options={chartData.apiChartData!.options}
/>
</div>
</div>
</div>
</div>
{/* 두번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 승인 대기 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">승인 대기 알림</h3>
</div>
<div className="p-4">
<div className="flex gap-4">
{/* 사용자 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-red-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-red-500"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
</div>
<span className="text-2xl font-medium">사용자 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">
{data?.waitMemberCnt}건
</div>
<div className="mt-2 text-2xl text-gray-500">
신규 가입 승인 대기
</div>
</div>
</div>
{/* 매뉴 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-blue-500"
>
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</div>
<span className="text-2xl font-medium">메뉴 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">3건</div>
<div className="mt-2 text-2xl text-gray-500">
매뉴 수정 승인 대기
</div>
</div>
</div>
{/* 컨텐츠 승인 */}
<div className="flex flex-1 flex-col items-start rounded-md bg-accent p-6">
<div className="flex items-center">
<div className="mr-4 flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-yellow-500"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<span className="text-2xl font-medium">컨텐츠 승인</span>
</div>
<div className="mt-5">
<div className="text-5xl font-bold">
{data?.waitCntnCnt}건
</div>
<div className="mt-2 text-2xl text-gray-500">
컨텐츠 수정 승인 대기
</div>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-1">
{/* 실시간 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">실시간 알림</h3>
</div>
<div className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">신규 사용자 가입</span>
<span>1분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">메뉴 수정 요청</span>
<span>5분 전</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">게시물 신고</span>
<span>10분 전</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">컨텐츠 승인 요청</span>
<span>15분 전</span>
</div>
</div>
</div>
</div>
{/* 일정 알림 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">일정 알림</h3>
</div>
<div className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">3월 정기 시스템 점검</span>
<span>내일</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">서버 업데이트 예정</span>
<span>3일 후</span>
</div>
<div className="flex items-center justify-between border-b border-border pb-4">
<span className="font-medium">백업 스케줄</span>
<span>5일 후</span>
</div>
<div className="flex items-center justify-between">
<span className="font-medium">정기 보안 점검</span>
<span>7일 후</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 세번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 접속자 통계 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">접속자 통계</h3>
</div>
<div className="h-80 p-4">
<Line
data={{
...chartData.siteAccessChartData!.data,
datasets: processSiteAccessData(data?.siteAccessList ?? []),
}}
options={chartData.siteAccessChartData!.options}
/>
</div>
</div>
{/* 메뉴별 접속 통계 */}
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium">메뉴별 접속 통계</h3>
</div>
<div className="h-80 p-4">
<Bar
data={{
labels:
data?.menuAccessList?.map((item) => item.menuNm) || [],
datasets: processMenuAccessData(data?.menuAccessList || []),
}}
options={chartData.menuAccessChartData!.options}
/>
</div>
</div>
</div>
{/* 네번째 라인 */}
<div className="grid grid-cols-2 gap-1">
{/* 공지사항 */}
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none">
<h3 className="text-2xl font-medium">공지사항</h3>
</div>
{noticePostTableComponent}
</div>
{/* 신규 게시글 */}
<div className="card card-border !p-0">
<div className="card-header !m-0 !border-none">
<h3 className="text-2xl font-medium">신규 게시글</h3>
</div>
{recentPostTableComponent}
</div>
</div>
</div>
</div>
);
} }
export { DashboardPage }; export { DashboardPage };
+37 -24
View File
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { import {
Button, Button,
DateRangePicker,
Input, Input,
InputDatePicker,
Label, Label,
Select, Select,
SelectContent, SelectContent,
@@ -22,7 +22,7 @@ import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/loginlog/model/table-columns"; import { columns } from "@/pages/loginlog/model/table-columns";
type DateRange = { type DateRange = {
from: Date | undefined; from?: Date | undefined;
to?: Date | undefined; to?: Date | undefined;
}; };
@@ -32,13 +32,12 @@ function LoginlogPage() {
to: dayjs().toDate(), to: dayjs().toDate(),
}; };
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [isManualLoading, setIsManualLoading] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}); });
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState(""); const [userId, setUserId] = useState("");
const [errOcrnYn, setErrOcrnYn] = useState("N"); const [errOcrnYn, setErrOcrnYn] = useState("N");
const [searchParams, setSearchParams] = useState<LoginlogParams>({ const [searchParams, setSearchParams] = useState<LoginlogParams>({
@@ -48,6 +47,7 @@ function LoginlogPage() {
searchDateTo: dayjs().format("YYYY-MM-DD"), searchDateTo: dayjs().format("YYYY-MM-DD"),
sort: "lgnDt,desc", sort: "lgnDt,desc",
}); });
const [isManualLoading, setIsManualLoading] = useState(false);
const query = useQuery({ const query = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination], queryKey: [ENDPOINT.USER_SERVICE.LOGINLOG, searchParams, pagination],
@@ -88,8 +88,12 @@ function LoginlogPage() {
...searchParams, ...searchParams,
userId, userId,
errOcrnYn, errOcrnYn,
searchDateFrom: dayjs(dateRange.from).format("YYYY-MM-DD"), searchDateFrom: dateRange.from
searchDateTo: dayjs(dateRange.to).format("YYYY-MM-DD"), ? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
searchDateTo: dateRange.to
? dayjs(dateRange.to).format("YYYY-MM-DD")
: "",
}); });
query.refetch().finally(() => { query.refetch().finally(() => {
setIsManualLoading(false); setIsManualLoading(false);
@@ -133,6 +137,7 @@ function LoginlogPage() {
default: default:
presetDateRange = undefined; presetDateRange = undefined;
} }
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange); setDateRange(presetDateRange || defaultDateRange);
}; };
@@ -141,23 +146,31 @@ function LoginlogPage() {
<div className="card card-border"> <div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label> <Label className="mr-2">조회기간</Label>
<DateRangePicker <Select value={presetDateRange} onValueChange={handlePresetDateRange}>
value={dateRange} <SelectTrigger className="w-[12rem]">
onValueChange={(value) => setDateRange(value || defaultDateRange)} <SelectValue placeholder="기간 선택" className="w-[24rem]" />
> </SelectTrigger>
<div className="card card-border flex items-center gap-2"> <SelectContent>
<Select onValueChange={handlePresetDateRange}> <SelectItem value="1W">일주일</SelectItem>
<SelectTrigger> <SelectItem value="1M">1개월</SelectItem>
<SelectValue placeholder="기간 선택" className="w-[24rem]" /> <SelectItem value="3M">3개월</SelectItem>
</SelectTrigger> </SelectContent>
<SelectContent> </Select>
<SelectItem value="1W">일주일</SelectItem> <InputDatePicker
<SelectItem value="1M">1개월</SelectItem> value={dateRange.from}
<SelectItem value="3M">3개월</SelectItem> onChange={(date) => {
</SelectContent> setDateRange({ ...dateRange, from: date });
</Select> setPresetDateRange("");
</div> }}
</DateRangePicker> />
~
<InputDatePicker
value={dateRange.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label> <Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input <Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]" className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
+1
View File
@@ -110,6 +110,7 @@ function MenuPage() {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
}); });
form.reset();
}, },
}); });
@@ -6,8 +6,8 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { import {
Button, Button,
DateRangePicker,
Input, Input,
InputDatePicker,
Label, Label,
Select, Select,
SelectContent, SelectContent,
@@ -24,7 +24,7 @@ import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/privacylog/model/table-columns"; import { columns } from "@/pages/privacylog/model/table-columns";
type DateRange = { type DateRange = {
from: Date | undefined; from?: Date | undefined;
to?: Date | undefined; to?: Date | undefined;
}; };
@@ -38,6 +38,7 @@ function PrivacyLogPage() {
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}); });
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange); const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [inqInfoKeyword, setInqInfoKeyword] = useState(""); const [inqInfoKeyword, setInqInfoKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({ const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
@@ -85,8 +86,10 @@ function PrivacyLogPage() {
setSearchParams({ setSearchParams({
...searchParams, ...searchParams,
inqInfoKeyword, inqInfoKeyword,
inqDtStart: dayjs(dateRange.from).format("YYYY-MM-DD"), inqDtStart: dateRange.from
inqDtEnd: dayjs(dateRange.to).format("YYYY-MM-DD"), ? dayjs(dateRange.from).format("YYYY-MM-DD")
: "",
inqDtEnd: dateRange.to ? dayjs(dateRange.to).format("YYYY-MM-DD") : "",
}); });
}; };
@@ -125,6 +128,7 @@ function PrivacyLogPage() {
default: default:
presetDateRange = undefined; presetDateRange = undefined;
} }
setPresetDateRange(value);
setDateRange(presetDateRange || defaultDateRange); setDateRange(presetDateRange || defaultDateRange);
}; };
@@ -133,30 +137,37 @@ function PrivacyLogPage() {
<div className="card card-border"> <div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label> <Label className="mr-2">조회기간</Label>
<DateRangePicker <Select value={presetDateRange} onValueChange={handlePresetDateRange}>
value={dateRange} <SelectTrigger className="w-[12rem]">
onValueChange={(value) => setDateRange(value || defaultDateRange)} <SelectValue placeholder="기간 선택" className="w-[24rem]" />
> </SelectTrigger>
<div className="card card-border flex items-center gap-2"> <SelectContent>
<Select onValueChange={handlePresetDateRange}> <SelectItem value="1W">일주일</SelectItem>
<SelectTrigger> <SelectItem value="1M">1개월</SelectItem>
<SelectValue placeholder="기간 선택" className="w-[24rem]" /> <SelectItem value="3M">3개월</SelectItem>
</SelectTrigger> </SelectContent>
<SelectContent> </Select>
<SelectItem value="1W">일주일</SelectItem> <InputDatePicker
<SelectItem value="1M">1개월</SelectItem> value={dateRange.from}
<SelectItem value="3M">3개월</SelectItem> onChange={(date) => {
</SelectContent> setDateRange({ ...dateRange, from: date });
</Select> setPresetDateRange("");
</div> }}
</DateRangePicker> />
~
<InputDatePicker
value={dateRange.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">조회정보</Label> <Label className="ml-4 mr-2 text-label">조회정보</Label>
<Input <Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]" className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={inqInfoKeyword} value={inqInfoKeyword}
onChange={(e) => setInqInfoKeyword(e.target.value)} onChange={(e) => setInqInfoKeyword(e.target.value)}
/> />
<Button className="h-[3.2rem]" type="submit"> <Button className="h-[3.2rem]" type="submit">
조회 조회
</Button> </Button>
+33 -25
View File
@@ -12,8 +12,8 @@ import {
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
Button, Button,
DateRangePicker,
Input, Input,
InputDatePicker,
Label, Label,
Select, Select,
SelectContent, SelectContent,
@@ -37,7 +37,7 @@ import { columns } from "../model/table-columns";
import { MovePostDialog } from "./move-post-dialog"; import { MovePostDialog } from "./move-post-dialog";
type DateRange = { type DateRange = {
from: Date | undefined; from?: Date | undefined;
to?: Date | undefined; to?: Date | undefined;
}; };
@@ -51,22 +51,21 @@ function RecentPage() {
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage); const alert = useAlertStore((state) => state.setMessage);
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [tableSort, setTableSort] = useState("regDt,desc"); const [tableSort, setTableSort] = useState("regDt,desc");
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}); });
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState(""); const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState(""); const [keyword, setKeyword] = useState("");
const [bbsId, setBbsId] = useState<string | null>(null); const [bbsId, setBbsId] = useState<string | null>(null);
const [delYn, setDelYn] = useState("N"); const [delYn, setDelYn] = useState("N");
const [searchParams, setSearchParams] = useState<RecentPostParams>({ const [searchParams, setSearchParams] = useState<RecentPostParams>({
keywordType: "", keywordType: "",
keyword: "", keyword: "",
@@ -193,6 +192,7 @@ function RecentPage() {
default: default:
presetDateRange = undefined; presetDateRange = undefined;
} }
setPresetDateRange(value);
setDateRange(presetDateRange); setDateRange(presetDateRange);
}; };
@@ -250,21 +250,31 @@ function RecentPage() {
<div className="card card-border"> <div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2"> <form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">날짜</Label> <Label className="mr-2">날짜</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}> <Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<div className="card card-border flex items-center gap-2"> <SelectTrigger className="w-[12rem]">
<Select onValueChange={handlePresetDateRange}> <SelectValue placeholder="기간 선택" className="w-[24rem]" />
<SelectTrigger> </SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" /> <SelectContent>
</SelectTrigger> <SelectItem value="1W">일주일</SelectItem>
<SelectContent> <SelectItem value="1M">1개월</SelectItem>
<SelectItem value="1W">일주일</SelectItem> <SelectItem value="3M">3개월</SelectItem>
<SelectItem value="1M">1개월</SelectItem> </SelectContent>
<SelectItem value="3M">3개월</SelectItem> </Select>
</SelectContent> <InputDatePicker
</Select> value={dateRange?.from}
</div> onChange={(date) => {
</DateRangePicker> setDateRange({ ...dateRange, from: date });
setPresetDateRange("");
}}
/>
~
<InputDatePicker
value={dateRange?.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">게시판</Label> <Label className="ml-4 mr-2 text-label">게시판</Label>
<Select <Select
value={bbsId?.toString() || ""} value={bbsId?.toString() || ""}
@@ -281,7 +291,6 @@ function RecentPage() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Label className="ml-4 mr-2 text-label">상태</Label> <Label className="ml-4 mr-2 text-label">상태</Label>
<Select value={delYn} onValueChange={(value) => setDelYn(value)}> <Select value={delYn} onValueChange={(value) => setDelYn(value)}>
<SelectTrigger className="w-[10rem]"> <SelectTrigger className="w-[10rem]">
@@ -292,7 +301,6 @@ function RecentPage() {
<SelectItem value="Y">삭제</SelectItem> <SelectItem value="Y">삭제</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Label className="ml-4 mr-2 text-label">검색</Label> <Label className="ml-4 mr-2 text-label">검색</Label>
<Select <Select
value={keywordType} value={keywordType}
+32 -19
View File
@@ -8,8 +8,8 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { import {
Button, Button,
Checkbox, Checkbox,
DateRangePicker,
Input, Input,
InputDatePicker,
Label, Label,
Select, Select,
SelectContent, SelectContent,
@@ -28,7 +28,7 @@ import { useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns"; import { columns } from "../model/table-columns";
type DateRange = { type DateRange = {
from: Date | undefined; from?: Date | undefined;
to?: Date | undefined; to?: Date | undefined;
}; };
@@ -39,6 +39,7 @@ function UsersPage() {
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 10,
}); });
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange | undefined>({ const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined, from: undefined,
to: undefined, to: undefined,
@@ -167,6 +168,7 @@ function UsersPage() {
default: default:
presetDateRange = undefined; presetDateRange = undefined;
} }
setPresetDateRange(value);
setDateRange(presetDateRange); setDateRange(presetDateRange);
}; };
@@ -277,23 +279,34 @@ function UsersPage() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="mr-2 text-label">기간</Label> <Label className="mr-2 text-label">기간</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}> <Select
<div className="card card-border flex items-center gap-2"> value={presetDateRange}
<Select onValueChange={handlePresetDateRange}> onValueChange={handlePresetDateRange}
<SelectTrigger> >
<SelectValue <SelectTrigger className="w-[12rem]">
placeholder="기간 선택" <SelectValue placeholder="기간 선택" className="w-[24rem]" />
className="w-[24rem]" </SelectTrigger>
/> <SelectContent>
</SelectTrigger> <SelectItem value="1M">1개월</SelectItem>
<SelectContent> <SelectItem value="3M">3개월</SelectItem>
<SelectItem value="1M">1개월</SelectItem> <SelectItem value="6M">6개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem> </SelectContent>
<SelectItem value="6M">6개월</SelectItem> </Select>
</SelectContent> <InputDatePicker
</Select> value={dateRange?.from}
</div> onChange={(date) => {
</DateRangePicker> setDateRange({ ...dateRange, from: date });
setPresetDateRange("");
}}
/>
~
<InputDatePicker
value={dateRange?.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+1
View File
@@ -23,6 +23,7 @@ export const ENDPOINT = {
PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs", PRIVACYLOG: "/user-service//api/v1/prvc-inq-logs",
}, },
CMS_SERVICE: { CMS_SERVICE: {
DASHBOARD: "/cms-service/api/v1/dashboard",
SITES: "/cms-service/api/v1/sites", SITES: "/cms-service/api/v1/sites",
IPS: "/cms-service/api/v1/site-ips", IPS: "/cms-service/api/v1/site-ips",
DOMAINS: "/cms-service/api/v1/site-dmns", DOMAINS: "/cms-service/api/v1/site-dmns",
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+8 -8
View File
@@ -1,13 +1,13 @@
NEXT_PUBLIC_SITE_ID = SN 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_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-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
+2
View File
@@ -20,7 +20,9 @@
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"chart.js": "^4.4.8",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
@@ -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_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; GisApp.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_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키
GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; 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_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
+1
View File
@@ -57,6 +57,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
@@ -1,5 +1,6 @@
import { getApiClient } from "./client"; import { getApiClient } from "./client";
import { ENDPOINT } from "../config"; import { ENDPOINT } from "../config";
import { getConfig } from "@common/config";
export interface MenuItem { export interface MenuItem {
menuCd: number; menuCd: number;
@@ -26,6 +27,8 @@ export interface TreeMenu extends MenuItem {
} }
const client = getApiClient(); const client = getApiClient();
const { PROXY_HOST } = getConfig();
const BASE_URL = `${PROXY_HOST}/server`;
export const getMenuList = async (siteId: string) => { export const getMenuList = async (siteId: string) => {
const response = await client.get<MenuItem[]>( const response = await client.get<MenuItem[]>(
@@ -40,3 +43,18 @@ export const getPreviewTreeMenuList = async (siteId: string) => {
); );
return response.data; return response.data;
}; };
export const getAnonymouseMenuList = async (
siteId: string,
): Promise<MenuItem[]> => {
const response = await fetch(
`${BASE_URL}/${ENDPOINT.CMS_SERVICE.MENU_ANONYMOUS}/user/${siteId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
return response.json();
};
+12 -5
View File
@@ -1,4 +1,4 @@
import { getApiClient } from "./client"; import { getConfig } from "@common/config";
import { ENDPOINT } from "../config"; import { ENDPOINT } from "../config";
export interface SiteInfo { export interface SiteInfo {
@@ -23,11 +23,18 @@ export interface SiteInfo {
mdfrNm: string; mdfrNm: string;
} }
const client = getApiClient(); const { PROXY_HOST } = getConfig();
const BASE_URL = `${PROXY_HOST}/server`;
export const getSiteInfo = async (siteId: string) => { export const getSiteInfo = async (siteId: string) => {
const response = await client.get<SiteInfo>( const response = await fetch(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, `${BASE_URL}/${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
); );
return response.data; return response.json();
}; };
@@ -2,6 +2,7 @@ export const ENDPOINT = {
CMS_SERVICE: { CMS_SERVICE: {
MENUS: "/cms-service/api/v1/menus", MENUS: "/cms-service/api/v1/menus",
MENU_ROLES: "/cms-service/api/v1/menu-roles", MENU_ROLES: "/cms-service/api/v1/menu-roles",
MENU_ANONYMOUS: "/cms-service/api/v1/menu/anonymous",
SITES: "/cms-service/api/v1/sites", SITES: "/cms-service/api/v1/sites",
POPUPS: "/cms-service/api/v1/popups", POPUPS: "/cms-service/api/v1/popups",
IPS: "/cms-service/api/v1/site-ips", IPS: "/cms-service/api/v1/site-ips",
@@ -3,6 +3,7 @@ import "./header.css";
import { useEffect, useCallback, useState } from "react"; import { useEffect, useCallback, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { images } from "@common/assets"; import { images } from "@common/assets";
import { getConfig } from "@common/config";
import { ENDPOINT } from "../../config"; import { ENDPOINT } from "../../config";
import { setCookie, getCookie } from "../../lib"; import { setCookie, getCookie } from "../../lib";
import { import {
@@ -22,7 +23,10 @@ import { Button } from "../button";
import { SidebarTrigger } from "../sidebar"; import { SidebarTrigger } from "../sidebar";
import { Skeleton } from "../skeleton"; 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 }) { export function Header({ viewOnly }: { viewOnly?: boolean }) {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@@ -30,7 +34,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
const { data: userInfo } = useQuery({ const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS], queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () => getUserInfo(), queryFn: () => getUserInfo(),
enabled: !viewOnly, enabled: isLoggedIn && !viewOnly,
}); });
const { data: siteInfo } = useQuery({ const { data: siteInfo } = useQuery({
@@ -50,6 +54,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
const { data: userPopupList } = useQuery({ const { data: userPopupList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS], queryKey: [ENDPOINT.CMS_SERVICE.POPUPS],
queryFn: () => getUserPopupList(), queryFn: () => getUserPopupList(),
enabled: isLoggedIn && !viewOnly,
}); });
const logout = async () => { const logout = async () => {
@@ -130,30 +135,36 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
</span> </span>
</h1> </h1>
</div> </div>
<DropdownMenu> {mounted ? (
<DropdownMenuTrigger asChild> isLoggedIn && (
<Button <DropdownMenu>
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label" <DropdownMenuTrigger asChild>
data-trigger="dropdown" <Button
> className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" /> data-trigger="dropdown"
{userInfo?.userName} >
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" /> <i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
</Button> {userInfo?.userName}
</DropdownMenuTrigger> <i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
<DropdownMenuContent> </Button>
<DropdownMenuItem>프로필 설정</DropdownMenuItem> </DropdownMenuTrigger>
<div className="flex flex-col items-start gap-0 px-4 py-3"> <DropdownMenuContent>
<p className="text-base text-label">최종접속일</p> <DropdownMenuItem>프로필 설정</DropdownMenuItem>
<p>{userInfo?.lastLoginDate}</p> <div className="flex flex-col items-start gap-0 px-4 py-3">
</div> <p className="text-base text-label">최종접속일</p>
<div className="flex flex-col items-start gap-0 px-4 py-3"> <p>{userInfo?.lastLoginDate}</p>
<p className="text-base text-label">접속 IP</p> </div>
<p></p> <div className="flex flex-col items-start gap-0 px-4 py-3">
</div> <p className="text-base text-label">접속 IP</p>
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem> <p></p>
</DropdownMenuContent> </div>
</DropdownMenu> <DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
) : (
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
)}
</header> </header>
); );
} }
@@ -7,6 +7,7 @@ export * from "./carousel";
export * from "./checkbox"; export * from "./checkbox";
export * from "./data-table"; export * from "./data-table";
export * from "./date-picker"; export * from "./date-picker";
export * from "./input-date-picker";
export * from "./date-range-picker"; export * from "./date-range-picker";
export * from "./dialog"; export * from "./dialog";
export * from "./dropdown-menu"; export * from "./dropdown-menu";
@@ -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<Date | undefined>(
defaultValue,
);
const [month, setMonth] = useState<Date | undefined>(
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 (
<div className="flex flex-col gap-3">
<div className="relative flex gap-2">
<Input
id="date"
type="date"
value={displayValue ? dayjs(displayValue).format("YYYY-MM-DD") : ""}
disabled={disabled}
className={`input-date-picker bg-background pr-10 ${className || ""}`}
onChange={(e) => {
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}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
id="date-picker"
variant="ghost"
color="white"
disabled={disabled}
className="absolute right-2 top-1/2 size-7 -translate-y-1/2 rounded-[0.4rem] border-none p-0"
>
<CalendarIcon className="!size-full" />
<span className="sr-only">Select date</span>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="end"
alignOffset={-8}
sideOffset={10}
>
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
month={month}
onMonthChange={setMonth}
onSelect={(newDate) => {
handleDateChange(newDate);
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
</div>
);
}
function isValidDate(date: Date | undefined) {
if (!date) {
return false;
}
return !isNaN(date.getTime());
}
@@ -0,0 +1,7 @@
@tailwind components;
@layer components {
.input-date-picker::-webkit-calendar-picker-indicator {
display: none;
}
}
+42 -12
View File
@@ -3,6 +3,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import { getConfig } from "@common/config";
import { getParentIds } from "./lib"; import { getParentIds } from "./lib";
import { import {
Sidebar, Sidebar,
@@ -22,7 +23,17 @@ import {
} from "../collapsible"; } from "../collapsible";
import { ENDPOINT } from "../../config"; import { ENDPOINT } from "../../config";
import { cn, getCookie, setCookie } from "../../lib"; 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({ export function LNB({
viewOnly, viewOnly,
@@ -42,24 +53,34 @@ export function LNB({
handleMenuClick?: (item: MenuItem) => void; handleMenuClick?: (item: MenuItem) => void;
}) { }) {
const [defaultOpenMenuIds, setDefaultOpenMenuIds] = useState<number[]>([]); const [defaultOpenMenuIds, setDefaultOpenMenuIds] = useState<number[]>([]);
const [currentMenuCode, setCurrentMenuCode] = useState<number | null>(null);
const prevPathRef = useRef(currentPath); const prevPathRef = useRef(currentPath);
const cookieSiteId = getCookie("site-id-key") ?? ""; const siteId = propSiteId ?? appSiteId;
const siteId = propSiteId ?? cookieSiteId;
const { data: menuListData } = useQuery({ const { data: menuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
queryFn: () => getMenuList(siteId), queryFn: () => getMenuList(siteId),
enabled: !viewOnly && !previewMode, enabled: isLoggedIn && !viewOnly && !previewMode,
}); });
const { data: previewTreeMenuListData } = useQuery({ const { data: previewTreeMenuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId, "preview"], queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId, "preview"],
queryFn: () => getPreviewTreeMenuList(siteId), 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) { function isPathMatched(path: string) {
if (!path) return false; if (!path) return false;
@@ -70,6 +91,7 @@ export function LNB({
if (!menuList || !currentPath) return; if (!menuList || !currentPath) return;
const matchedMenuIds: number[] = []; const matchedMenuIds: number[] = [];
let matchedMenuCode: number | null = null;
const findMatchedMenus = (items: MenuItem[]) => { const findMatchedMenus = (items: MenuItem[]) => {
items.forEach((item) => { items.forEach((item) => {
@@ -77,25 +99,32 @@ export function LNB({
const parentIds = getParentIds(item.menuCd)(menuList); const parentIds = getParentIds(item.menuCd)(menuList);
matchedMenuIds.push(...parentIds, item.menuCd); matchedMenuIds.push(...parentIds, item.menuCd);
} }
if (item.children) findMatchedMenus(item.children);
});
};
if (item.children) { const findMatchedMenuCode = (items: MenuItem[]) => {
findMatchedMenus(item.children); items.forEach((item) => {
} if (matchedMenuCode) return;
if (currentPath === item.urlPath) matchedMenuCode = item.menuCd;
if (item.children) findMatchedMenuCode(item.children);
}); });
}; };
findMatchedMenus(menuList); findMatchedMenus(menuList);
findMatchedMenuCode(menuList);
if (matchedMenuIds.length > 0) { if (matchedMenuIds.length > 0)
setDefaultOpenMenuIds((prev) => [ setDefaultOpenMenuIds((prev) => [
...new Set([...prev, ...matchedMenuIds]), ...new Set([...prev, ...matchedMenuIds]),
]); ]);
}
if (prevPathRef.current !== currentPath) { if (prevPathRef.current !== currentPath) {
setDefaultOpenMenuIds(matchedMenuIds); setDefaultOpenMenuIds(matchedMenuIds);
prevPathRef.current = currentPath; prevPathRef.current = currentPath;
} }
if (matchedMenuCode) setCurrentMenuCode(matchedMenuCode);
}, [menuList, currentPath]); }, [menuList, currentPath]);
function MenuItem({ function MenuItem({
@@ -106,7 +135,8 @@ export function LNB({
isSubMenu?: boolean; isSubMenu?: boolean;
}) { }) {
const { open: isSidebarOpen, setOpen } = useSidebar(); const { open: isSidebarOpen, setOpen } = useSidebar();
const isMatched = isPathMatched(item.urlPath); const isMatched =
isPathMatched(item.urlPath) && currentMenuCode === item.menuCd;
const handleClick = () => { const handleClick = () => {
if (!isSidebarOpen) setOpen(true); if (!isSidebarOpen) setOpen(true);
+10836
View File
File diff suppressed because it is too large Load Diff