Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/sn/dev

This commit is contained in:
2025-07-24 22:06:34 +09:00
45 changed files with 2558 additions and 1041 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 {
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<DateRange>(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() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDatePicker
value={dateRange.from}
onChange={(date) => {
setDateRange({ ...dateRange, from: date });
setPresetDateRange("");
}}
/>
~
<InputDatePicker
value={dateRange.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
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() {
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 };
+37 -24
View File
@@ -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<DateRange>(defaultDateRange);
const [isManualLoading, setIsManualLoading] = useState(false);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [userId, setUserId] = useState("");
const [errOcrnYn, setErrOcrnYn] = useState("N");
const [searchParams, setSearchParams] = useState<LoginlogParams>({
@@ -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() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDatePicker
value={dateRange.from}
onChange={(date) => {
setDateRange({ ...dateRange, from: date });
setPresetDateRange("");
}}
/>
~
<InputDatePicker
value={dateRange.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
<Label className="ml-4 mr-2 text-label">사용자ID</Label>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
+1
View File
@@ -110,6 +110,7 @@ function MenuPage() {
queryClient.invalidateQueries({
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 {
Button,
DateRangePicker,
Input,
InputDatePicker,
Label,
Select,
SelectContent,
@@ -24,7 +24,7 @@ import { ENDPOINT } from "@/shared/config";
import { columns } from "@/pages/privacylog/model/table-columns";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -38,6 +38,7 @@ function PrivacyLogPage() {
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
const [inqInfoKeyword, setInqInfoKeyword] = useState("");
const [searchParams, setSearchParams] = useState<PrivacyLogParams>({
@@ -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() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">조회기간</Label>
<DateRangePicker
value={dateRange}
onValueChange={(value) => setDateRange(value || defaultDateRange)}
>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDatePicker
value={dateRange.from}
onChange={(date) => {
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>
<Input
className="h-[3.2rem] w-[20rem] rounded-sm !text-[1.3rem]"
value={inqInfoKeyword}
onChange={(e) => setInqInfoKeyword(e.target.value)}
/>
<Button className="h-[3.2rem]" type="submit">
조회
</Button>
+33 -25
View File
@@ -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<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [tableSort, setTableSort] = useState("regDt,desc");
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
const [keywordType, setKeywordType] = useState("");
const [keyword, setKeyword] = useState("");
const [bbsId, setBbsId] = useState<string | null>(null);
const [delYn, setDelYn] = useState("N");
const [searchParams, setSearchParams] = useState<RecentPostParams>({
keywordType: "",
keyword: "",
@@ -193,6 +192,7 @@ function RecentPage() {
default:
presetDateRange = undefined;
}
setPresetDateRange(value);
setDateRange(presetDateRange);
};
@@ -250,21 +250,31 @@ function RecentPage() {
<div className="card card-border">
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
<Label className="mr-2">날짜</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select value={presetDateRange} onValueChange={handlePresetDateRange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1W">일주일</SelectItem>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
</SelectContent>
</Select>
<InputDatePicker
value={dateRange?.from}
onChange={(date) => {
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>
<Select
value={bbsId?.toString() || ""}
@@ -281,7 +291,6 @@ function RecentPage() {
))}
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">상태</Label>
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
<SelectTrigger className="w-[10rem]">
@@ -292,7 +301,6 @@ function RecentPage() {
<SelectItem value="Y">삭제</SelectItem>
</SelectContent>
</Select>
<Label className="ml-4 mr-2 text-label">검색</Label>
<Select
value={keywordType}
+32 -19
View File
@@ -8,8 +8,8 @@ import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import {
Button,
Checkbox,
DateRangePicker,
Input,
InputDatePicker,
Label,
Select,
SelectContent,
@@ -28,7 +28,7 @@ import { useDataTable } from "@/shared/lib";
import { columns } from "../model/table-columns";
type DateRange = {
from: Date | undefined;
from?: Date | undefined;
to?: Date | undefined;
};
@@ -39,6 +39,7 @@ function UsersPage() {
pageIndex: 0,
pageSize: 10,
});
const [presetDateRange, setPresetDateRange] = useState("");
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
@@ -167,6 +168,7 @@ function UsersPage() {
default:
presetDateRange = undefined;
}
setPresetDateRange(value);
setDateRange(presetDateRange);
};
@@ -277,23 +279,34 @@ function UsersPage() {
</div>
<div className="flex items-center gap-2">
<Label className="mr-2 text-label">기간</Label>
<DateRangePicker value={dateRange} onValueChange={setDateRange}>
<div className="card card-border flex items-center gap-2">
<Select onValueChange={handlePresetDateRange}>
<SelectTrigger>
<SelectValue
placeholder="기간 선택"
className="w-[24rem]"
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
<SelectItem value="6M">6개월</SelectItem>
</SelectContent>
</Select>
</div>
</DateRangePicker>
<Select
value={presetDateRange}
onValueChange={handlePresetDateRange}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="기간 선택" className="w-[24rem]" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1M">1개월</SelectItem>
<SelectItem value="3M">3개월</SelectItem>
<SelectItem value="6M">6개월</SelectItem>
</SelectContent>
</Select>
<InputDatePicker
value={dateRange?.from}
onChange={(date) => {
setDateRange({ ...dateRange, from: date });
setPresetDateRange("");
}}
/>
~
<InputDatePicker
value={dateRange?.to}
onChange={(date) => {
setDateRange({ ...dateRange, to: date });
setPresetDateRange("");
}}
/>
</div>
</div>
<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",
},
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",
+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_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
@@ -376,7 +376,7 @@ export const MsSwageMhlCol = [
{ id: 'vrtc_len', title: '세로길이(m)' },
// { id: 'ang_dir', title: '표고' },
{ id: 'altd_vl', title: '표고(EL.m)' },
{ id: 'dpthlVl', title: '심도(m)' },
{ id: 'dpthl_vl', title: '심도(m)' },
// { id: 'inst_mng_no', title: '기관관리번호' }
];
export const MsSwageMhlCol2 = [
@@ -507,7 +507,7 @@ export const ExclnSewagePumpCol2 = [
// { id: 'instMngNo', title: '기관관리번호' },
]
// 오수펌프장
export const DrtwSewagePumpCol1 = [
export const DrtwSewagePumpCol = [
// { id: 'fid', title: 'FID' },
// { id: 'geom', title: 'geometry' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
@@ -574,16 +574,16 @@ export const stofcbrCol = [
{ id: 'drtn_angl', title: '방향각도(°)' },
]
export const stofcbrCol2 = [
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
{ id: 'pbadmsEmdCd', title: '시설관리번호' },
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
{ id: 'fcltMngNo', title: '시설관리번호' },
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
{ id: 'mapshtNo', title: '도엽번호' },
// { id: 'mng_inst_cd', title: '관리기관코드' },
// { id: 'mngInstCd', title: '관리기관코드' },
{ id: 'instlYmd', title: '설치일자' },
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
// { id: 'fcltyShpCd', title: '시설물형태코드' },
{ id: 'worflwWallHgt', title: '월류벽높이(m)' },
// { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' },
// { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' },
// { id: 'mhlExstYnCd', title: '맨홀존재여부코드' },
// { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' },
{ id: 'swageMhlMngNo', title: '하수맨홀관리번호' },
{ id: 'drtnAngl', title: '방향각도(°)' },
]
@@ -6,7 +6,7 @@ import { useGisContext } from "@common/business_components/ui/map/Gis";
import { Button } from "@common/components/ui";
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { getLayerContentComponent } from "./components/common";
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol1, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "./components/common/table";
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "./components/common/table";
import InfilChart from './components/popupComponent/chart/infilChart';
import RdiiChart from './components/popupComponent/chart/rdiiChart';
import { useInfilChartModalStore, useRdiiChartModalStore } from "./components/popupComponent/chartModalStore";
@@ -14,7 +14,6 @@ import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { codeMapper } from "./components/common/codeMapper";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
@@ -59,59 +58,124 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
let result = null;
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수
layers.forEach((layer: any) => {
layers.some((layer: any) => {
const source = layer.getSource();
// console.log('실행됨3 source', source);
// console.log('실행됨3 featureData', featureData);
// console.log('실행됨3 featureData.fid', featureData.fid);
// if (source && featureData && featureData.fid) {
// featureLayer = layer; // 피처가 속한 레이어 찾기
// }
if (source && source.hasFeature && featureData
&& typeof featureData.getId === 'function' && source.hasFeature(featureData)) {
featureLayer = layer; // 피처가 속한 레이어 찾기
return true; // 반복 종료
}
return false;
});
// console.log("featureLayer.get('lyrTyNm');", featureLayer.get('lyrTyNm'));
// SearchModal 클릭 이벤트
if (featureLayer.get('lyrNm') === null) {
setIsClicked(false);
if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설';
else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역';
else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') result = '우수토실';
else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val);
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(false);
} else {
setIsDrtwSewagePump(true);
}
result = '하수펌프장';
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') result = '하수맨홀';
}
else {
if (featureData['ol_uid']) {
// Gis 레이어 클릭 이벤트
setIsClicked(true);
const lyrTyNm = featureLayer.get('lyrTyNm');
if (lyrTyNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설';
else if (lyrTyNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역';
else if (lyrTyNm === 'tb_ms_stofcbr') result = '우수토실';
else if (lyrTyNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (lyrTyNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (lyrTyNm === 'tb_ms_swage_pmstn') {
console.log('gis data', featureData.values_);
const lyrTyNm = featureData.id_.split('.')[0];
if (lyrTyNm === 'tb_ms_swage_ppl') {
result = '하수관로';
setHeadlist(MsSwagePplCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'tb_ms_swage_mhl') {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'tb_ms_swage_pmstn') { // 하수펌프장
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val);
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(false);
} else {
setIsDrtwSewagePump(true);
setHeadlist(ExclnSewagePumpCol);
}
else {
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol);
}
setLayerSpecificNm(featureData.values_.fclt_mng_no);
result = '하수펌프장';
}
else if (lyrTyNm === 'tb_ms_swage_mhl') result = '하수맨홀';
else if (lyrTyNm === 'tb_il_swage_prcs_fclt') {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
}
else if (lyrTyNm === 'tb_ms_swage_prcs_zone') {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol);
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
}
else if (lyrTyNm === 'tb_ms_stofcbr') {
result = '우수토실';
setHeadlist(stofcbrCol);
setLayerSpecificNm(featureData.values_.fclt_mng_no);
}
else if (lyrTyNm === 'om_dt_untrt_swg_rain') {
result = '중점우수토실';
setHeadlist(untrtSwgRainCol1);
setHeadlist2(untrtSwgRainCol2);
setLayerSpecificNm(featureData.values_.manage_no);
}
else result = featureLayer.get('lyrNm');
}
else {
// SearchModal 클릭 이벤트
setIsClicked(false);
console.log('SearchModal data', featureData);
const lyrTyNm = featureData.lyrTypeNm;
console.log('실행됨4 SearchModal 클릭 이벤트', lyrTyNm);
console.log('실행됨4 SearchModal 클릭 이벤트 featureData', featureData);
if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') {
result = '하수관로';
setHeadlist(MsSwagePplCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') {
result = '하수맨홀';
setHeadlist(MsSwageMhlCol2)
setLayerSpecificNm(featureData.fcltMngNo);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') { // 하수펌프장
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) {
console.log('오수펌프장');
setIsDrtwSewagePump(false);
setHeadlist(ExclnSewagePumpCol2)
}
else {
console.log('빗물펌프장');
setIsDrtwSewagePump(true);
setHeadlist(DrtwSewagePumpCol2)
}
setLayerSpecificNm(featureData.fcltMngNo);
result = '하수펌프장';
}
else if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') {
result = '하수처리시설';
setHeadlist(SwtrmtFcltCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') {
result = '하수처리구역';
setHeadlist(SwtrmtPrcsZoneCol2);
setLayerSpecificNm(featureData.swtrmtFcltCd);
}
else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') {
result = '우수토실';
setHeadlist(stofcbrCol2)
setLayerSpecificNm(featureData.fcltMngNo);
}
// else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
}
return result;
// return featureLayer.get('lyrNm'); // 레이어 이름 출력
};
useEffect(() => {
@@ -122,8 +186,10 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
console.log('gis data', featureData.values_);
const data = featureData.values_;
// console.log('gis data', featureData.values_);
let data;
if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때
else data = featureData; // SearchModal 클릭 이벤트일 때
// 하수맨홀인 경우 용도코드를 텍스트로 변환
if (selectedLayerName === '하수맨홀' && data) {
@@ -139,16 +205,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
// 🎯 기본 데이터 포맷팅
const formattedData = formatDisplayData(data);
if (featureData?.values_?.fclt_mng_no) setLayerSpecificNm(featureData.values_.fclt_mng_no);
else if (featureData?.values_?.swtrmt_fclt_cd) setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
else setLayerSpecificNm(featureData?.values_?.manage_no);
setFetchData(formattedData);
setTableData(formattedData);
switch (selectedLayerName) {
case '하수관로':
if (isClicked) setHeadlist(MsSwagePplCol)
else setHeadlist(MsSwagePplCol2)
setButtonChk(true)
// ✅ codeMapper 사용하여 API 결과도 변환
@@ -156,12 +217,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
layerName: selectedLayerName,
data: { ...formattedData }
}));
// setFetchData(mappedData);
// setTableData(mappedData);
break;
case '하수맨홀':
if (isClicked) setHeadlist(MsSwageMhlCol)
else setHeadlist(MsSwageMhlCol2)
setButtonChk(true)
// ✅ codeMapper 사용하여 API 결과도 변환
@@ -169,46 +226,26 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
layerName: selectedLayerName,
data: { ...formattedData }
}));
// setFetchData(mappedData);
// setTableData(mappedData);
break;
case '하수펌프장':
if (isDrtwSewagePump) {
if (isClicked) setHeadlist(DrtwSewagePumpCol1)
else setHeadlist(DrtwSewagePumpCol2)
} else {
if (isClicked) setHeadlist(ExclnSewagePumpCol)
else setHeadlist(ExclnSewagePumpCol2)
}
setButtonChk(true)
setTableData(codeMapper({
layerName: selectedLayerName,
data: { ...formattedData }
}));
// setFetchData(mappedData);
// setTableData(mappedData);
break;
case '하수처리시설':
if (isClicked) setHeadlist(SwtrmtFcltCol)
else setHeadlist(SwtrmtFcltCol2)
setButtonChk(false)
break;
case '하수처리구역':
if (isClicked) setHeadlist(SwtrmtPrcsZoneCol)
else setHeadlist(SwtrmtPrcsZoneCol2)
setButtonChk(false)
break;
case '우수토실':
if (isClicked) setHeadlist(stofcbrCol)
else setHeadlist(stofcbrCol2)
setButtonChk(true)
setIsMainSsc(false)
break;
case '중점우수토실':
setHeadlist(untrtSwgRainCol1)
setHeadlist2(untrtSwgRainCol2)
setButtonChk(false)
setIsMainSsc(true)
// 🎯 processedData도 포맷팅하여 설정
@@ -0,0 +1,199 @@
import React from 'react';
import { MS_SWAGE_MHL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox';
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import SwagePpl from './swagePpl';
import SwagePrcsFclt from './swagePrcsFclt';
import SwagePrcsZone from './swagePrcsZone';
import SwageMhl from './swageMhl';
import SwagePmstn from './swagePmstn';
// 기본정보 '구분' 선택 목록
export const BASIC_TYPES = [
{ val: "tb_ms_swage_ppl", text: "하수관로" },
{ val: "tb_il_swage_prcs_fclt", text: "하수처리시설" },
{ val: "tb_ms_swage_prcs_zone", text: "하수처리구역" },
{ val: "tb_ms_swage_mhl", text: "하수맨홀" },
{ val: "tb_ms_swage_pmstn", text: "하수펌프장" },
{ val: "tb_ms_stofcbr", text: "우수토실" },
];
interface BasicTabProps {
open?: boolean;
basicType: string;
city: number;
sgg: number;
start1: string;
start2: string;
end1: string;
end2: string;
var1: string;
var2: string;
yearFilter: { start: string; end: string; };
handleSearch: () => void;
// onOpenChange?: (open: boolean) => void;
// onFeatureSelect?: (row: any) => void;
// setTableData?: (data: any[]) => void;
// setTableCount?: (count: number) => void;
setBasicType: (type: string) => void;
setCity: (city: number) => void;
setSgg: (sgg: number) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
setStart1: (value: string) => void;
setStart2: (value: string) => void;
setEnd1: (value: string) => void;
setEnd2: (value: string) => void;
setVar1: (value: string) => void;
setVar2: (value: string) => void;
}
const BasicTab: React.FC<BasicTabProps> = ({
// open,
basicType,
city,
sgg,
start1,
start2,
end1,
end2,
var1,
var2,
yearFilter,
handleSearch,
// onOpenChange,
// onFeatureSelect,
// setTableData,
// setTableCount,
setBasicType,
setCity,
setSgg,
setYearFilter,
setStart1,
setStart2,
setEnd1,
setEnd2,
setVar1,
setVar2,
}) => {
return (
<div className="relative border rounded-lg p-6 mt-4 w-full h-[10vh]">
{/* 구분 */}
<div className="mb-3 flex items-center justify-between gap-4">
<div className="flex items-center">
<SelectBox
className="w-[150px] mr-3"
selectClass="rounded-lg bg-white"
label="구분"
selectArray={BASIC_TYPES}
onSelectValue={(value: string | number) =>
setBasicType(String(value))
}
value={basicType}
selectContentClass="z-[101]"
/>
</div>
</div>
{/* 분기처리: 하수관로 */}
{basicType === "tb_ms_swage_ppl" && (
<SwagePpl
var1={var1}
var2={var2}
start1={start1}
end1={end1}
yearFilter={yearFilter}
setVar1={setVar1}
setVar2={setVar2}
setStart1={setStart1}
setEnd1={setEnd1}
setYearFilter={setYearFilter}
handleSearch={handleSearch}
/>
)}
{/* 분기처리: 하수처리시설 */}
{basicType === "tb_il_swage_prcs_fclt" && (
<SwagePrcsFclt
var1={var1}
start1={start1}
start2={start2}
end1={end1}
end2={end2}
yearFilter={yearFilter}
setVar1={setVar1}
setStart1={setStart1}
setStart2={setStart2}
setEnd1={setEnd1}
setEnd2={setEnd2}
setYearFilter={setYearFilter}
handleSearch={handleSearch}
/>
)}
{/* 분기처리: 하수처리구역 */}
{basicType === "tb_ms_swage_prcs_zone" && (
<SwagePrcsZone
var1={var1}
var2={var2}
city={city}
sgg={sgg}
setVar1={setVar1}
setVar2={setVar2}
setCity={setCity}
setSgg={setSgg}
handleSearch={handleSearch}
/>
)}
{/* 분기처리: 하수맨홀 */}
{basicType === "tb_ms_swage_mhl" && (
<SwageMhl
var1={var1}
start1={start1}
end1={end1}
yearFilter={yearFilter}
setVar1={setVar1}
setStart1={setStart1}
setEnd1={setEnd1}
setYearFilter={setYearFilter}
handleSearch={handleSearch}
/>
)}
{/* 분기처리: 하수펌프장 */}
{basicType === "tb_ms_swage_pmstn" && (
<SwagePmstn
start1={start1}
start2={start2}
end1={end1}
end2={end2}
yearFilter={yearFilter}
setStart1={setStart1}
setStart2={setStart2}
setEnd1={setEnd1}
setEnd2={setEnd2}
setYearFilter={setYearFilter}
handleSearch={handleSearch}
/>
)}
{/* 분기처리: 우수토실 */}
{basicType === "tb_ms_stofcbr" && (
<div className="mb-3">
{/* 설치년도 */}
<div className="flex items-center mb-2">
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="설치년도"
endLabel=""
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
)}
</div>
);
};
export default React.memo(BasicTab);
@@ -0,0 +1,162 @@
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import React from 'react';
//import { useDcTree, flattenFolders, useSggCd, useCityCd } from './components/searchModalReportApi';
// import { useBizByFcltStore } from "../../../store/StoreProvider";
// 운영정보 '구분' 선택 목록
// const WORK_TYPES = [
// { val: "maintenance", text: "보수 및 갱신" },
// { val: "cleaning", text: "청소 및 준설" },
// { val: "inspection", text: "점검 및 진단" },
// ];
export const WORK_TYPES = [
{ val: "200", text: "보수 및 갱신(개대체)" },
{ val: "201", text: "보수 및 갱신(유지보수)" },
{ val: "204", text: "보수 및 갱신(단순보수)" },
{ val: "100", text: "점검 및 진단" },
{ val: "203", text: "청소 및 준설" },
];
// const BIZ_SE_CD = useBizByFcltStore(state => state.BIZ_SE_CD);
export const BIZ_SE_CD = [
{ lwpoCd: "100", lwpoCdNm: "관로기술진단" },
{ lwpoCd: "200", lwpoCdNm: "개대체" },
{ lwpoCd: "201", lwpoCdNm: "유지보수" },
{ lwpoCd: "203", lwpoCdNm: "청소, 준설" },
{ lwpoCd: "204", lwpoCdNm: "단순보수" },
];
// const BIZ_TY_CD = useBizByFcltStore(state => state.BIZ_TY_CD);
export const BIZ_TY_CD = [
{ lwpoCd: "0", lwpoCdNm: "전체" },
{ lwpoCd: "SV", lwpoCdNm: "용역" },
{ lwpoCd: "SF", lwpoCdNm: "자체" },
{ lwpoCd: "SC", lwpoCdNm: "공사" },
];
export const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({
val: item.lwpoCd,
text: item.lwpoCdNm,
}));
// 운영정보 - 사업 현황
export const operationTableHeadlist = [
{ id: "bizNm", title: "사업명" },
{ id: "flfmtBgngYmd", title: "수행시작일자" },
{ id: "flfmtEndYmd", title: "수행종료일자" },
{ id: "flfmtYr", title: "수행년도" },
{ id: "bizTyCd", title: "사업유형" },
{ id: "bizAmt", title: "사업금액" },
];
// 운영정보 - 보수 및 갱신 결과표
export const operationMaintenanceTableHeadlist = [
{ id: "fcltMngNo", title: "관리번호" },
{ id: "usgCd", title: "용도" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmt", title: "관경(mm)" },
{ id: "prlg", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
// { id: "instlYmd", title: "사업구분" },
];
// 운영정보 - 청소 및 준설 결과표
export const operationCleaningTableHeadlist = [
{ id: "fcltMngNo", title: "관리번호" },
{ id: "usgCd", title: "용도" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmt", title: "관경(mm)" },
{ id: "prlg", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
];
// 운영정보 - 점검 및 진단 결과표
export const operationInspectionTableHeadlist = [
{ id: "fcltMngNo", title: "관리번호" },
{ id: "usgCd", title: "용도" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmt", title: "관경(mm)" },
{ id: "prlg", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
{ id: "strct_stat_grd", title: "구조적 상태등급" },
{ id: "oper_stat_grd", title: "운영적 상태등급" },
];
interface OperationTabProps {
open: boolean;
workType: string;
var1: string;
var2: string;
yearFilter: { start: string; end: string; };
handleSearch: () => void;
// onOpenChange: (open: boolean) => void;
// setTableData: (data: any[]) => void;
// setTableCount: (count: number) => void;
setWorkType: (type: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
setVar1: (value: string) => void;
setVar2: (value: string) => void;
}
const OperationTab: React.FC<OperationTabProps> = ({
open,
workType,
var1,
var2,
yearFilter,
handleSearch,
// onOpenChange,
// setTableData,
// setTableCount,
setWorkType,
setVar1,
setVar2,
setYearFilter,
}) => {
return (
<div className="relative flex flex-col border rounded-lg p-6 mt-4 w-full h-[10vh]">
<div className="flex items-center mb-3">
<SelectBox
className="w-80 mr-8"
selectClass="rounded-lg bg-white text-xl"
label="업무구분"
selectArray={WORK_TYPES.map((w) => ({
val: w.val,
text: w.text,
}))}
onSelectValue={(val) => setWorkType(String(val))}
value={workType}
selectContentClass="z-[101]"
/>
</div>
<div className="flex items-center mb-6">
<Label className="mr-2 w-auto text-xl text-gray-600">사업명</Label>
<Input
className="w-[100px] !text-xl placeholder:text-center mr-4"
placeholder="입력"
value={var1}
onChange={(e) => { setVar1(e.target.value); }}
/>
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="수행기간"
endLabel=""
/>
<SelectBox
className="ml-8 w-60 mr-8"
selectClass="rounded-lg bg-white text-xl"
label="사업 유형"
selectArray={BIZ_TY_CD_SELECT_ARRAY}
value={var2}
onSelectValue={(val) => {
setVar2(String(val));
}}
selectContentClass="z-[101]"
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
)
};
export default React.memo(OperationTab);
@@ -0,0 +1,109 @@
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input } from '@common/components';
import React from 'react';
//import { useDcTree, flattenFolders, useSggCd, useCityCd } from './components/searchModalReportApi';
import { type SelectDataType } from '../commonStore';
import { useCityCd, useSggCd } from '../components/searchModalReportApi';
// import { useBizByFcltStore } from "../../../store/StoreProvider";
// // 하수도정비 기본계획 보고서
export const reportTableHeadlist = [
{ id: "no", title: "NO" },
{ id: "sido", title: "시도" },
{ id: "sigungu", title: "시군구" },
{ id: "approvalYear", title: "승인년도" },
{ id: "plantZoneGugun", title: "구분" },
{ id: "reportGubun", title: "보고서구분" },
{ id: "fileName", title: "파일명" },
{ id: "regDate", title: "등록일자" },
];
interface BasicTabProps {
open?: boolean;
gubun: string;
sidos: SelectDataType[];
ctpvCd: string | number;
sigungu: string;
filteredSiguns: SelectDataType[];
yearFilter: { start: string; end: string; };
handleSearch: () => void;
handleCtpvCdChange: (val: string | number) => void;
// onOpenChange?: (open: boolean) => void;
// onFeatureSelect?: (row: any) => void;
// setTableData?: (data: any[]) => void;
// setTableCount?: (count: number) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
setGubun: (gubun: string) => void;
setSigungu: (sigungu: string) => void;
}
const ReportTab: React.FC<BasicTabProps> = ({
open,
gubun,
sidos,
ctpvCd,
sigungu,
filteredSiguns = [{ val: '0', text: '전체', sido: '0' }],
yearFilter,
handleSearch,
handleCtpvCdChange,
// onOpenChange,
// onFeatureSelect,
// setTableData,
// setTableCount,
setYearFilter,
setGubun,
setSigungu,
}) => {
return (
<div className="relative flex flex-col border rounded-lg p-6 mt-4 w-full h-[10vh]">
<div className="flex items-center mb-3">
<SelectBox
className="w-[150px] mr-8"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidos}
value={ctpvCd}
onSelectValue={handleCtpvCdChange}
selectContentClass="z-[101]"
/>
<SelectBox
className="w-48"
selectClass="w-48 rounded-lg bg-white truncate"
label="시군구"
selectArray={filteredSiguns}
value={sigungu}
onSelectValue={val => setSigungu(String(val))}
selectContentClass="z-[101]"
/>
</div>
<div className="flex items-center mb-6">
<label className="inline-block w-auto whitespace-nowrap text-xl text-gray-600 mr-2">구분</label>
<Input
className='!text-xl w-[100px] placeholder:text-center mr-8'
placeholder="입력"
value={gubun}
onChange={(e) => { setGubun(e.target.value); }}
/>
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="승인년도"
endLabel=""
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(ReportTab);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,38 @@
// 우수토실
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { Button } from '@common/components';
import React from 'react';
interface StofcbrProps {
yearFilter: { start: string; end: string };
setYearFilter: (filter: { start: string; end: string }) => void;
handleSearch: () => void;
}
const Stofcbr: React.FC<StofcbrProps> = ({
yearFilter,
setYearFilter,
handleSearch,
}) => {
return (
<div className="mb-3">
{/* 설치년도 */}
<div className="flex items-center mb-2">
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="설치년도"
endLabel=""
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(Stofcbr);
@@ -0,0 +1,94 @@
// 하수맨홀
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { Button, Input, Label } from '@common/components';
import React from 'react';
import { MS_SWAGE_MHL_USE_CODES } from '../components/common/table';
import { SelectBox } from '@common/business_components/ui';
interface SwageMhlProps {
var1: string;
start1: string;
end1: string;
yearFilter: { start: string; end: string };
setVar1: (value: string) => void;
setStart1: (value: string) => void;
setEnd1: (value: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
handleSearch: () => void;
}
const SwageMhl: React.FC<SwageMhlProps> = ({
var1,
start1,
end1,
yearFilter,
setVar1,
setStart1,
setEnd1,
setYearFilter,
handleSearch,
}) => {
return (
<div className="mb-3">
<div className="mb-2 flex items-center">
{/* 설치년도 */}
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={(range) =>
setYearFilter({
...yearFilter,
start: range.start,
end: range.end,
})
}
startLabel="설치년도"
endLabel=""
/>
{/* 용도 */}
<SelectBox
className="ml-8 w-60 mr-8"
selectClass="rounded-lg bg-white text-xl"
label="용도"
selectArray={MS_SWAGE_MHL_USE_CODES}
value={var1}
onSelectValue={(val) => {
setVar1(String(val));
}}
selectContentClass="z-[101]"
/>
{/* 하수맨홀 구경 */}
<Label className="mr-2 w-auto text-xl text-gray-600">구경(mm)</Label>
<Input
className="w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={start1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart1(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={end1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd1(val);
}}
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(SwageMhl);
@@ -0,0 +1,102 @@
// 하수펌프장
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { Button, Input, Label } from '@common/components';
import React from 'react';
interface SwagePmstnProps {
start1: string;
start2: string;
end1: string;
end2: string;
yearFilter: { start: string; end: string };
setStart1: (value: string) => void;
setStart2: (value: string) => void;
setEnd1: (value: string) => void;
setEnd2: (value: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
handleSearch: () => void;
}
const SwagePmstn: React.FC<SwagePmstnProps> = ({
start1,
start2,
end1,
end2,
yearFilter,
setStart1,
setStart2,
setEnd1,
setEnd2,
setYearFilter,
handleSearch,
}) => {
return (
<div className="mb-3">
<div className="flex items-center mb-2">
{/* 설치년도 */}
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="설치년도"
endLabel=""
/>
{/* 일일처리용량 - 오수맨홀 */}
<Label className="ml-8 mr-2 w-auto text-xl text-gray-600">일일처리용량(㎥)</Label>
<Input
className="w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={start1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart1(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={end1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd1(val);
}}
/>
{/* 양수능력 - 우수맨홀 */}
<Label className="mr-2 w-auto text-xl text-gray-600">
양수능력(m²/d)
</Label>
<Input
className="w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={start2}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart2(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={end2}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd2(val);
}}
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(SwagePmstn);
@@ -0,0 +1,132 @@
// 하수관로
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import React from 'react';
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
interface SwagePplProps {
var1: string;
var2: string;
start1: string;
end1: string;
yearFilter: { start: string; end: string };
setVar1: (value: string) => void;
setVar2: (value: string) => void;
setStart1: (value: string) => void;
setEnd1: (value: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
handleSearch: () => void;
}
const SwagePpl: React.FC<SwagePplProps> = ({
var1,
var2,
start1,
end1,
yearFilter,
setVar1,
setVar2,
setStart1,
setEnd1,
setYearFilter,
handleSearch,
}) => {
return (
<div className="mb-6">
<div className="mb-2 flex items-center">
{/* 설치년도 */}
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={(range) =>
setYearFilter({
...yearFilter,
start: range.start,
end: range.end,
})
}
startLabel="설치년도"
endLabel=""
/>
{/* 용도 */}
<SelectBox
className="ml-8 w-60 mr-8"
selectClass="rounded-lg bg-white text-xl"
label="용도"
selectArray={MS_SWAGE_PPL_USE_CODES}
value={var1}
onSelectValue={(val) => {
setVar1(String(val));
}}
selectContentClass="z-[101]"
/>
{/* 재질 */}
<SelectBox
className="w-60 mr-8"
selectClass="rounded-lg bg-white text-xl"
label="재질"
selectArray={MS_SWAGE_PPL_CODES}
value={var2}
onSelectValue={(val) => {
setVar2(String(val));
}}
selectContentClass="z-[101]"
/>
{/* 관경 */}
<Label className="mr-2 w-auto text-xl text-gray-600">관경(mm)</Label>
<Input
className="w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={var1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setVar1(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={end1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd1(val);
}}
/>
{/* 연장 */}
<Label className="mr-2 w-auto text-xl text-gray-600">연장(m)</Label>
<Input
className="w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={start1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart1(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={end1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd1(val);
}}
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(SwagePpl);
@@ -0,0 +1,121 @@
// 하수처리시설
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { Button, Input, Label } from '@common/components';
import React from 'react';
interface SwagePrcsFcltProps {
var1: string;
start1: string;
start2: string;
end1: string;
end2: string;
yearFilter: { start: string; end: string };
setVar1: (value: string) => void;
setStart1: (value: string) => void;
setStart2: (value: string) => void;
setEnd1: (value: string) => void;
setEnd2: (value: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
handleSearch: () => void;
}
const SwagePrcsFclt: React.FC<SwagePrcsFcltProps> = ({
var1,
start1,
start2,
end1,
end2,
yearFilter,
setVar1,
setStart1,
setStart2,
setEnd1,
setEnd2,
setYearFilter,
handleSearch,
}) => {
return (
<div className="mb-3">
{/* 가동개시일자 */}
<div className="mb-2 flex items-center">
<Label className="mr-2 w-auto text-xl text-gray-600">가동개시일자</Label>
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={(range) =>
setYearFilter({
...yearFilter,
start: range.start,
end: range.end,
})
}
startLabel=""
endLabel=""
/>
{/* 시설용량 */}
<Label className="ml-8 mr-2 w-auto text-xl text-gray-600">시설용량(㎥/d)</Label>
<Input
className="w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={start1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart1(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={end1}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd1(val);
}}
/>
{/* 처리구역 면적 */}
<Label className="mr-2 w-auto text-xl text-gray-600">처리구역 면적(m²)</Label>
<Input
className="w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={start2}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setStart2(val);
}}
/>
<span className="mx-2 text-xl font-semibold">~</span>
<Input
className="mr-8 w-[60px] !text-xl placeholder:text-center"
placeholder="입력"
value={end2}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setEnd2(val);
}}
/>
{/* 하수처리시설명 */}
<Label className="mr-2 w-auto text-xl text-gray-600">하수처리시설명</Label>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={var1}
onChange={(e) => {
setVar1(e.target.value);
}}
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(SwagePrcsFclt);
@@ -0,0 +1,69 @@
// 하수처리구역
import { SidoSigunguSelectBox } from '@/components/ui/SidoSigunguSelectBox';
import { Button, Input, Label } from '@common/components';
import React from 'react';
interface swagePrcsZoneProps {
var1: string;
var2: string;
city: number;
sgg: number;
setVar1: (value: string) => void;
setVar2: (value: string) => void;
setCity: (value: number) => void;
setSgg: (value: number) => void;
handleSearch: () => void;
}
const swagePrcsZone: React.FC<swagePrcsZoneProps> = ({
var1,
var2,
city,
sgg,
setVar1,
setVar2,
setCity,
setSgg,
handleSearch,
}) => {
return (
<div className="mb-3">
<div className="mb-2 flex items-center">
<SidoSigunguSelectBox
sido={city}
sigungu={sgg}
onSidoChange={(sidoId) => {
setCity(sidoId);
setSgg(0);
}}
onSigunguChange={setSgg}
/>
<Label className="mr-2 w-auto text-xl text-gray-600">하수처리구역명</Label>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={var1}
onChange={(e) => {
setVar1(e.target.value);
}}
/>
<Label className="mr-2 w-auto text-xl text-gray-600">하수처리시설명</Label>
<Input
className='!text-xl w-[120px] placeholder:text-center mr-8'
placeholder="입력"
value={var2}
onChange={(e) => {
setVar2(e.target.value);
}}
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
);
};
export default React.memo(swagePrcsZone);
@@ -4,7 +4,6 @@ import ContentLayout from '@/components/ui/contentLayout'
import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm'
import { DatePicker, DateMonthPicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { SelectBox2 } from '@common/business_components/ui/select/index-2'
import { Button, Label } from '@common/components/ui'
import { useEffect, useState, type ReactNode } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
@@ -259,8 +258,8 @@ const BaseLayout = ({
setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)}
setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달
>
<SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} />
{/* <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> */}
<SelectBox label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} selectContentClass="z-[101]" />
{/* <SelectBox label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} selectContentClass="z-[101]" /> */}
</GovSidoSigunguSearchForm>
{selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null}
</div>
+22 -4
View File
@@ -5,11 +5,29 @@ const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || '';
export const getData = async (url: string, data?: object): Promise<any> => {
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, {
method: "GET",
body: JSON.stringify(data),
});
if (res.ok) {
const data = await res.json();
return data;
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') {
return null;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') {
return null;
}
// JSON 파싱 시도
const data = JSON.parse(text);
return data;
} catch (error) {
console.error('JSON parsing error:', error);
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
throw new Error('Invalid JSON response from server');
}
}
throw new Error('Failed to fetch data');
};
@@ -93,7 +111,7 @@ export const getCmsData = async (url: string, data?: object): Promise<any> => {
throw new Error('Failed to fetch data');
};
export function getCookie(name:string) {
export function getCookie(name: string) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
if (match && match[2]) {
return decodeURIComponent(match[2]);
@@ -1,5 +1,5 @@
import React, { useMemo } from "react";
import { SelectBox2 } from "@common/business_components/ui/select/index-2";
import { SelectBox } from '@common/business_components/ui'
interface Option {
id: number;
@@ -138,21 +138,23 @@ export function SidoSigunguSelectBox({
return (
<div className={`flex items-center justify-center ${className} mr-8`}>
<SelectBox2
<SelectBox
className="w-[150px] mr-8"
selectClass="rounded-lg bg-white"
label="시도"
selectArray={sidoList.map(opt => ({ val: opt.id, text: opt.name }))}
value={sido}
onSelectValue={val => onSidoChange(Number(val))}
selectContentClass="z-[101]"
/>
<SelectBox2
<SelectBox
className="w-48"
selectClass="w-48 rounded-lg bg-white truncate"
label="시군구"
selectArray={sigunguList.map(opt => ({ val: opt.id, text: opt.name }))}
value={sigungu}
onSelectValue={val => onSigunguChange(Number(val))}
selectContentClass="z-[101]"
/>
</div>
);
@@ -1,5 +1,5 @@
import React from "react";
import { SelectBox2 } from "@common/business_components/ui/select/index-2";
import { SelectBox } from '@common/business_components/ui'
interface YearRangeSelectBoxProps {
start: string;
@@ -89,22 +89,24 @@ export function YearRangeSelectBox({
return (
<div className={`flex items-center ${className}`}>
<SelectBox2
<SelectBox
className="flex-1 mr-3"
selectClass={selectClass}
label={startLabel}
selectArray={getStartYearArray()}
onSelectValue={val => onChange({ start: String(val), end })}
value={start}
selectContentClass="z-[101]"
/>
<span className="font-semibold text-xl mx-2">~</span>
<SelectBox2
<SelectBox
className="flex-1 ml-1"
selectClass={selectClass}
label={endLabel}
selectArray={getEndYearArray()}
onSelectValue={val => onChange({ start, end: String(val) })}
value={end}
selectContentClass="z-[101]"
/>
</div>
);
+2
View File
@@ -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"
},
@@ -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';
@@ -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';
@@ -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));
+1
View File
@@ -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",
@@ -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<MenuItem[]>(
@@ -40,3 +43,18 @@ export const getPreviewTreeMenuList = async (siteId: string) => {
);
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";
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<SiteInfo>(
`${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();
};
@@ -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",
@@ -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 }) {
</span>
</h1>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
data-trigger="dropdown"
>
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">최종접속일</p>
<p>{userInfo?.lastLoginDate}</p>
</div>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">접속 IP</p>
<p></p>
</div>
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{mounted ? (
isLoggedIn && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="group m-0 flex h-[3rem] items-center !bg-background px-4 py-0 text-label"
data-trigger="dropdown"
>
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
{userInfo?.userName}
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>프로필 설정</DropdownMenuItem>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">최종접속일</p>
<p>{userInfo?.lastLoginDate}</p>
</div>
<div className="flex flex-col items-start gap-0 px-4 py-3">
<p className="text-base text-label">접속 IP</p>
<p></p>
</div>
<DropdownMenuItem onClick={logout}>로그아웃</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
) : (
<Skeleton className="mx-4 h-[3rem] w-[15rem]" />
)}
</header>
);
}
@@ -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";
@@ -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 { 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<number[]>([]);
const [currentMenuCode, setCurrentMenuCode] = useState<number | null>(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);