Compare commits
91
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a67f549f | ||
|
|
c0888ea2a4 | ||
|
|
1ead18b53c | ||
|
|
07db5b1fbd | ||
|
|
6110abebb4 | ||
|
|
05e9082831 | ||
|
|
76a634cf9a | ||
|
|
793c673414 | ||
|
|
2a76c279eb | ||
|
|
5d1ba3fd87 | ||
|
|
99db9a0b77 | ||
|
|
4126bc2234 | ||
|
|
d4f1ec095e | ||
|
|
bbf3ce3948 | ||
|
|
fb5b8356c7 | ||
|
|
1333bd040a | ||
|
|
013c07ef59 | ||
|
|
29fd59a321 | ||
|
|
3bc152c341 | ||
|
|
ea274f8f7d | ||
|
|
7ac53e56d7 | ||
|
|
7b1c12e952 | ||
|
|
17dafe6530 | ||
|
|
ef331363f7 | ||
|
|
4f10864c2c | ||
|
|
4b5ceb0e0a | ||
|
|
4fa50161fd | ||
|
|
3d27e07ccb | ||
|
|
ca1ca71a62 | ||
|
|
bdd6c7b804 | ||
|
|
04b8082f04 | ||
|
|
7bf8ec1d35 | ||
|
|
3451b19b96 | ||
|
|
ae7c1ea1c4 | ||
|
|
9c76b955e2 | ||
|
|
6c4bfe2798 | ||
|
|
98189cd14a | ||
|
|
823ce262bb | ||
|
|
34358400e8 | ||
|
|
da9d069a9a | ||
|
|
58a03678e4 | ||
|
|
f5c281cf86 | ||
|
|
c72bcea7d9 | ||
|
|
49cd160e76 | ||
|
|
5ca93a25ca | ||
|
|
55554fc781 | ||
|
|
8e78c10304 | ||
|
|
0eb74b46ba | ||
|
|
f21dde8f7d | ||
|
|
1fc6707bb3 | ||
|
|
44c5d66afa | ||
|
|
04edb49c9f | ||
|
|
e3eb542e59 | ||
|
|
428b199dd3 | ||
|
|
3c4a6efeea | ||
|
|
dae7024043 | ||
|
|
64e5c26e8c | ||
|
|
84ae60edd6 | ||
|
|
d4c9989069 | ||
|
|
6cda413913 | ||
|
|
a11eaf031d | ||
|
|
3c701359fd | ||
|
|
85ba9fe0ab | ||
|
|
71077f4c8e | ||
|
|
4c7ff53d78 | ||
|
|
b31691dd9c | ||
|
|
0131cae0d7 | ||
|
|
f37b19eab6 | ||
|
|
7c1db5e0e0 | ||
|
|
2c495ab8fd | ||
|
|
ced1b7084d | ||
|
|
b0c871eff7 | ||
|
|
bd83fa3417 | ||
|
|
c0b63a655e | ||
|
|
cc21aaede1 | ||
|
|
0c71c80a25 | ||
|
|
49e42757bc | ||
|
|
0964f442be | ||
|
|
73ceeb1b38 | ||
|
|
814365d7de | ||
|
|
8c1a976b5b | ||
|
|
66db81071e | ||
|
|
6df6e612bc | ||
|
|
3fee2a781f | ||
|
|
55839f0913 | ||
|
|
f68131fc13 | ||
|
|
7b9081a125 | ||
|
|
54d3a35b38 | ||
|
|
ba42b74578 | ||
|
|
e19057d026 | ||
|
|
f4c64168d1 |
Vendored
-20
@@ -23,26 +23,6 @@
|
||||
"NODE_OPTIONS": "--inspect"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 서버 컴포넌트 디버그 (node-terminal)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "dev:as 통합 디버그 (Server + Client)",
|
||||
"command": "pnpm --filter as dev",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"serverReadyAction": {
|
||||
"action": "debugWithChrome",
|
||||
"pattern": "- Local:\\s+(https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"webRoot": "${workspaceFolder}/apps/as"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
+1
-17
@@ -1,24 +1,8 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
import path from "path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@common/assets"],
|
||||
outputFileTracingRoot: path.join(__dirname, "../../"),
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
silent: true,
|
||||
widenClientFileUpload: true,
|
||||
tunnelRoute: "/monitoring",
|
||||
webpack: {
|
||||
treeshake: {
|
||||
removeDebugLogging: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true" ? false : true,
|
||||
},
|
||||
automaticVercelMonitors: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -20,13 +20,6 @@
|
||||
"@faker-js/faker": "^9.9.0",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/context-zone": "^2.8.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-fetch": "^0.219.0",
|
||||
"@opentelemetry/instrumentation-xml-http-request": "^0.219.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.8.0",
|
||||
"@sentry/nextjs": "^10.57.0",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -865,7 +865,7 @@ function CnfmViewPage({ id }: { id: string }) {
|
||||
<tr key={f.atflId}>
|
||||
<td>
|
||||
{f.orgnlFileNm ? (
|
||||
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/download/${f.atflId}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
|
||||
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
|
||||
) : (
|
||||
<span className="text-gray-700">{f.physFileNm || f.atflId}</span>
|
||||
)}
|
||||
|
||||
@@ -20,16 +20,6 @@ async function handler(
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
|
||||
// X-Device-Id 중계 로직 (헤더 또는 쿠키에서 획득)
|
||||
const deviceId =
|
||||
request.headers.get("x-device-id") ||
|
||||
request.headers.get("X-Device-Id") ||
|
||||
cookieStore.get("device-id")?.value;
|
||||
if (deviceId) {
|
||||
headers["X-Device-Id"] = deviceId;
|
||||
}
|
||||
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
|
||||
@@ -55,11 +45,6 @@ async function handler(
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
console.error(`[BFF Proxy Error] Backend returned status ${result.status} for ${params.url.join("/")}`);
|
||||
try {
|
||||
const errorText = await result.text();
|
||||
console.error(`[BFF Proxy Error Body] ${errorText}`);
|
||||
} catch (err) {}
|
||||
return NextResponse.json(
|
||||
{ message: "Invalid Credentials" },
|
||||
{ status: 401 },
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 1.0,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
await import("./sentry.edge.config");
|
||||
}
|
||||
}
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
+1
-14
@@ -1,5 +1,4 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { withSentryConfig } from "@sentry/nextjs";
|
||||
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
@@ -59,16 +58,4 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, {
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
silent: true,
|
||||
widenClientFileUpload: true,
|
||||
tunnelRoute: "/monitoring",
|
||||
webpack: {
|
||||
treeshake: {
|
||||
removeDebugLogging: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true" ? false : true,
|
||||
},
|
||||
automaticVercelMonitors: false,
|
||||
},
|
||||
});
|
||||
export default nextConfig;
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test:playwright": "playwright test"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
@@ -27,7 +26,6 @@
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/nextjs": "^10.57.0",
|
||||
"@tanstack/react-query": "^5.75.5",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
@@ -51,7 +49,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.0.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,60 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Playwright 설정 파일
|
||||
* https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* 하나의 테스트 파일 내에서 병렬 실행 금지 및 직렬 실행으로 세션 상태 격리 보장 */
|
||||
fullyParallel: false,
|
||||
/* Redis 및 PostgreSQL 공유 자원의 세션 정렬을 위해 워커를 1개로 제한 */
|
||||
workers: 1,
|
||||
/* 테스트 실패 시 재시도 횟수 */
|
||||
retries: 0,
|
||||
/* 각 테스트 타임아웃 (30초) */
|
||||
timeout: 30000,
|
||||
/* 전체 테스트 실행 타임아웃 */
|
||||
globalTimeout: 600000,
|
||||
/* Assertion 타임아웃 */
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
/* 리포터 설정 (HTML 리포트 및 터미널 요약) */
|
||||
reporter: [["html", { open: "never" }], ["list"]],
|
||||
/* 공유 설정 */
|
||||
use: {
|
||||
/* 테스트 타겟 URL */
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000",
|
||||
/* 실패한 테스트에 대해서만 트레이스 수집 */
|
||||
trace: "retain-on-failure",
|
||||
/* 스크린샷 캡처 설정 */
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
|
||||
/* 프로젝트별 브라우저 구성 (기본적으로 Chromium 위주로 동작 확인) */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* 로컬 서버 자동 기동 설정 */
|
||||
webServer: {
|
||||
command: "pnpm dev",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: true,
|
||||
timeout: 120000,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT ?? "local",
|
||||
tracesSampleRate: 1.0,
|
||||
debug: process.env.NEXT_PUBLIC_SENTRY_DEBUG === "true",
|
||||
});
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { SITE_ID, ENDPOINT } from "@/shared/config";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
@@ -17,10 +16,11 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const { data: menuRoles } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => MenuApi.getMenuRoles(SITE_ID),
|
||||
});
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
|
||||
const getPageMenuAuthority = async () => {
|
||||
const menuCd = getCookie("menu-code-key");
|
||||
@@ -35,15 +35,11 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return pageMenuAuthority?.inqAuthrtYn === "Y";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (menuRoles) {
|
||||
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
|
||||
if (currentMenu) setCurrentMenuInfo(currentMenu);
|
||||
}
|
||||
}, [menuRoles, pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const currentMenu = await getCurrentMenuInfo();
|
||||
if (currentMenu) setCurrentMenuInfo(currentMenu);
|
||||
|
||||
const response = await getPageMenuAuthority();
|
||||
if (response?.length) setPageMenuAuthority(response[0]!);
|
||||
})();
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type PaginationState,
|
||||
type Row,
|
||||
type RowSelectionState,
|
||||
type Table,
|
||||
type Row,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import {
|
||||
type AdminUserListResponse,
|
||||
UserApi,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
@@ -19,18 +31,10 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { GroupApi } from "@/entities/group";
|
||||
import {
|
||||
UserApi,
|
||||
type UserSearchParams,
|
||||
type AdminUserListResponse,
|
||||
} from "@/entities/user";
|
||||
import { GroupUsersApi } from "@/entities/group-user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useAlertStore } from "@/shared/lib";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
import { AddAdminDialog } from "./add-admin-dialog";
|
||||
|
||||
function AdminPage() {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { type Banner } from "../model/banner-interface";
|
||||
import { createColumns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
|
||||
import { PreviewBannerDialog } from "./preview-banner-dialog";
|
||||
|
||||
type PostingTarget = {
|
||||
@@ -22,50 +27,52 @@ type PostingTarget = {
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
bnrTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function BannerPage() {
|
||||
const router = useRouter();
|
||||
const imageRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [bnrTtl, setBnrTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<BannerParams>({
|
||||
bnrTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [bnrTtl, setBnrTtl] = useState(params.bnrTtl);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, params],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
bnrTtl: params.bnrTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bnrTtl,
|
||||
});
|
||||
setParams({ bnrTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setBnrTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bnrTtl: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
@@ -92,7 +99,7 @@ function BannerPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Board, BoardApi } from "@/entities/board";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -18,60 +25,57 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { BoardApi, type BoardParams } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { type Board } from "@/entities/board";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function BbsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [bbsTypeCd, setBbsTypeCd] = useState("");
|
||||
const [bbsNm, setBbsNm] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<BoardParams>({
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [bbsTypeCd, setBbsTypeCd] = useState(params.bbsTypeCd);
|
||||
const [bbsNm, setBbsNm] = useState(params.bbsNm);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, params],
|
||||
queryFn: () =>
|
||||
BoardApi.getBoardList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
bbsTypeCd: params.bbsTypeCd,
|
||||
bbsNm: params.bbsNm,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bbsTypeCd,
|
||||
bbsNm,
|
||||
});
|
||||
setParams({ bbsTypeCd, bbsNm, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setBbsTypeCd("");
|
||||
setBbsNm("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Board) => {
|
||||
@@ -89,7 +93,7 @@ function BbsPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { UpCode } from "@/entities/code";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { UpCodeManagementDialog } from "../ui/upcode-dialog";
|
||||
|
||||
export const columns: ColumnDef<UpCode>[] = [
|
||||
@@ -24,7 +27,7 @@ export const columns: ColumnDef<UpCode>[] = [
|
||||
header: "상위코드명",
|
||||
},
|
||||
{
|
||||
accessorKey: "cdSe",
|
||||
accessorKey: "cdSeNm",
|
||||
header: "코드구분",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,60 +2,64 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import {
|
||||
ContentsApi,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
} from "@/entities/content";
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Contents, ContentsApi } from "@/entities/content";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
contsTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ContentsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [contsTtl, setContsTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<ContentsParams>({
|
||||
contsTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [contsTtl, setContsTtl] = useState(params.contsTtl);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS, params],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
contsTtl: params.contsTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
contsTtl,
|
||||
});
|
||||
setParams({ contsTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setContsTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
contsTtl: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Contents) => {
|
||||
@@ -69,7 +73,7 @@ function ContentsPage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -354,7 +354,7 @@ function DashboardPage() {
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">실시간 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
|
||||
<div className="space-y-4 text-foreground">
|
||||
{noticeUserList?.map((item) => (
|
||||
<div
|
||||
@@ -373,7 +373,7 @@ function DashboardPage() {
|
||||
<div className="card-header text-foreground">
|
||||
<h3 className="text-2xl font-medium">일정 알림</h3>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="p-4" style={{ height: "120px", overflowY: "auto" }}>
|
||||
<div className="space-y-4 text-foreground">
|
||||
{scheduleList && scheduleList.length > 0 ? (
|
||||
scheduleList.map((schedule, index) => {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable, useMutationWrapper } from "@/shared/lib";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
function FilePage() {
|
||||
|
||||
@@ -2,68 +2,73 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { type Image } from "../model/image-interface";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, params],
|
||||
queryFn: () =>
|
||||
ImageApi.getImageList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
@@ -81,7 +86,7 @@ function ImagePage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -2,45 +2,76 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import {
|
||||
AuthorizationsApi,
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import {
|
||||
type Authorizations,
|
||||
type AuthorizationsParams,
|
||||
AuthorizationsApi,
|
||||
} from "@/entities/authorization";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/authorizations-table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function ModulePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [authorizationName, setAuthorizationName] = useState("");
|
||||
const [httpMethodCode, setHttpMethodCode] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<AuthorizationsParams>({
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [authorizationName, setAuthorizationName] = useState(
|
||||
params.authorizationName,
|
||||
);
|
||||
const [httpMethodCode, setHttpMethodCode] = useState(params.httpMethodCode);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, params],
|
||||
queryFn: () =>
|
||||
AuthorizationsApi.getAuthorizationsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
authorizationName: params.authorizationName,
|
||||
httpMethodCode: params.httpMethodCode,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ authorizationName, httpMethodCode, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setAuthorizationName("");
|
||||
setHttpMethodCode("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Authorizations) => {
|
||||
router.push(`/module/register/${row.authorizationNo}`);
|
||||
};
|
||||
@@ -52,7 +83,7 @@ function ModulePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
@@ -62,29 +93,6 @@ function ModulePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
authorizationName,
|
||||
httpMethodCode,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setAuthorizationName("");
|
||||
setHttpMethodCode("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
authorizationName: "",
|
||||
httpMethodCode: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
|
||||
@@ -2,70 +2,75 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { PopupApi } from "../api/popup-service";
|
||||
import type { Popup } from "../model/popup-interface";
|
||||
import { columns } from "../model/table-columns";
|
||||
import type { Popup, PopupParams } from "../model/popup-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function PopupPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<PopupParams>({
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POPUPS, params],
|
||||
queryFn: () =>
|
||||
PopupApi.getPopupList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Popup) => {
|
||||
@@ -82,7 +87,7 @@ function PopupPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
useAlertStore,
|
||||
useDataTable,
|
||||
useSearchParamsState,
|
||||
useSelectPrompt,
|
||||
} from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import {
|
||||
type DeletePost,
|
||||
deleteReasonMessages,
|
||||
PostApi,
|
||||
type RecentPost,
|
||||
} from "@/entities/post";
|
||||
import { SortSelect } from "@/widgets/post";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -20,76 +40,72 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { SortSelect } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
deleteReasonMessages,
|
||||
type DeletePost,
|
||||
type RecentPost,
|
||||
type RecentPostParams,
|
||||
} from "@/entities/post";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { useAlertStore, useSelectPrompt, useDataTable } from "@/shared/lib";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
import { MovePostDialog } from "./move-post-dialog";
|
||||
|
||||
type DateRange = {
|
||||
from?: Date | undefined;
|
||||
to?: Date | undefined;
|
||||
from?: Date | null | undefined;
|
||||
to?: Date | null | undefined;
|
||||
};
|
||||
|
||||
const selectData = Object.entries(deleteReasonMessages).map(
|
||||
([label, value]) => ({ label, value }),
|
||||
);
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: "",
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
|
||||
return {
|
||||
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function RecentPage() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { prompt } = useSelectPrompt(selectData);
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
|
||||
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>({
|
||||
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: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [isDeletePostData, setIsDeletePostData] = useState(false);
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
parseDateRange(params.frDateYmd, params.toDateYmd),
|
||||
);
|
||||
const [keywordType, setKeywordType] = useState(params.keywordType);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [bbsId, setBbsId] = useState(params.bbsId);
|
||||
const [delYn, setDelYn] = useState(params.delYn);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.BOARD_SERVICE.POSTS,
|
||||
searchParams,
|
||||
pagination,
|
||||
tableSort,
|
||||
],
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, params],
|
||||
queryFn: () =>
|
||||
PostApi.getRecentPostList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: tableSort,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
bbsId: params.bbsId ? parseInt(params.bbsId) : null,
|
||||
delYn: params.delYn,
|
||||
frDateYmd: params.frDateYmd,
|
||||
toDateYmd: params.toDateYmd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
@@ -99,9 +115,22 @@ function RecentPage() {
|
||||
queryFn: () => BoardApi.getBoardList(),
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const isDeletePostData = params.delYn === "Y";
|
||||
|
||||
const handleRowClick = (row: RecentPost) => {
|
||||
const from = `${pathname}${searchParams?.toString() ? `?${searchParams.toString()}` : ""}`;
|
||||
router.push(
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`,
|
||||
`/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}?from=${encodeURIComponent(from)}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -116,7 +145,7 @@ function RecentPage() {
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
@@ -127,45 +156,32 @@ function RecentPage() {
|
||||
|
||||
const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0;
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDeletePostData(delYn === "Y");
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
setParams({
|
||||
keywordType,
|
||||
keyword,
|
||||
bbsId: bbsId ? parseInt(bbsId) : null,
|
||||
bbsId,
|
||||
delYn,
|
||||
frDateYmd: dateRange?.from
|
||||
? dayjs(dateRange.from).format("YYYYMMDD")
|
||||
: "",
|
||||
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeywordType("");
|
||||
setKeyword("");
|
||||
setBbsId(null);
|
||||
setBbsId("");
|
||||
setDelYn("N");
|
||||
setPresetDateRange("");
|
||||
setDateRange({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType: "",
|
||||
keyword: "",
|
||||
bbsId: null,
|
||||
delYn: "N",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
setIsDeletePostData(false);
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
@@ -273,10 +289,7 @@ function RecentPage() {
|
||||
setDateRange={handleDateRangeChange}
|
||||
/>
|
||||
<Label className="ml-4 mr-2 text-label">게시판</Label>
|
||||
<Select
|
||||
value={bbsId?.toString() || ""}
|
||||
onValueChange={(value) => setBbsId(value ? value : null)}
|
||||
>
|
||||
<Select value={bbsId} onValueChange={setBbsId}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
@@ -289,7 +302,7 @@ function RecentPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Label className="ml-4 mr-2 text-label">상태</Label>
|
||||
<Select value={delYn} onValueChange={(value) => setDelYn(value)}>
|
||||
<Select value={delYn} onValueChange={setDelYn}>
|
||||
<SelectTrigger className="w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
@@ -376,10 +389,10 @@ function RecentPage() {
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(parseInt(value));
|
||||
}}
|
||||
value={params.size.toString()}
|
||||
onValueChange={(value) =>
|
||||
setParams({ size: parseInt(value), page: 0 })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
@@ -392,7 +405,10 @@ function RecentPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SortSelect value={tableSort} onValueChange={setTableSort} />
|
||||
<SortSelect
|
||||
value={params.sort}
|
||||
onValueChange={(value) => setParams({ sort: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,39 +2,68 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { RoleApi, type Role, type RoleParams } from "@/entities/role";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Role, RoleApi } from "@/entities/role";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function RolePage() {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<RoleParams>({
|
||||
keywordType: "roleName",
|
||||
keyword: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLES, params],
|
||||
queryFn: () =>
|
||||
RoleApi.getRoles({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ keyword, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Role) => {
|
||||
router.push(`/role/register/${row.roleId}`);
|
||||
};
|
||||
@@ -48,7 +77,7 @@ function RolePage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
@@ -56,26 +85,6 @@ function RolePage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
|
||||
@@ -1,54 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import "@common/assets/styles/grid.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { type Site, SiteApi } from "@/entities/site";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
siteNm: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function SitePage() {
|
||||
const router = useRouter();
|
||||
const [siteNm, setSiteNm] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<SiteParams>({
|
||||
siteNm: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [siteNm, setSiteNm] = useState(params.siteNm);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, params],
|
||||
queryFn: () =>
|
||||
SiteApi.getSiteList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
siteNm: params.siteNm,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
siteNm,
|
||||
});
|
||||
setParams({ siteNm, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSiteNm("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
siteNm: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Site) => {
|
||||
@@ -62,7 +73,7 @@ function SitePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { SurveyApi } from "../api/survey-service";
|
||||
import type { Survey, SurveyParams } from "../model/survey-interface";
|
||||
import type { Survey } from "../model/survey-interface";
|
||||
import { columns } from "../model/survey-table-columns";
|
||||
|
||||
type PostingTarget = {
|
||||
@@ -22,32 +26,53 @@ type PostingTarget = {
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
srvyTtl: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function SurveyPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [srvyTtl, setSrvyTtl] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<SurveyParams>({
|
||||
srvyTtl: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [srvyTtl, setSrvyTtl] = useState(params.srvyTtl);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SURVEY, params],
|
||||
queryFn: () =>
|
||||
SurveyApi.getSurveyList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
srvyTtl: params.srvyTtl,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setParams({ srvyTtl, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSrvyTtl("");
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Survey) => {
|
||||
router.push(`/survey/${row.srvySn}`);
|
||||
};
|
||||
@@ -62,7 +87,7 @@ function SurveyPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
},
|
||||
dataTableOptions: {
|
||||
@@ -71,26 +96,6 @@ function SurveyPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
srvyTtl,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSrvyTtl("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
srvyTtl: "",
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
|
||||
@@ -2,58 +2,70 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { TemplateApi } from "../api/template-service";
|
||||
import type { Template } from "../model/template-interface";
|
||||
import { columns } from "../model/template-table-columns";
|
||||
import type { Template, TemplateParams } from "../model/template-interface";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function TemplatePage() {
|
||||
const router = useRouter();
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [tmpltSeCd, setTmpltSeCd] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<TemplateParams>({
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [tmpltSeCd, setTmpltSeCd] = useState(params.tmpltSeCd);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, params],
|
||||
queryFn: () =>
|
||||
TemplateApi.getTemplateList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
tmpltSeCd: params.tmpltSeCd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
tmpltSeCd,
|
||||
});
|
||||
setParams({ keyword, tmpltSeCd, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setTmpltSeCd("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
tmpltSeCd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Template) => {
|
||||
@@ -67,7 +79,7 @@ function TemplatePage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -2,63 +2,70 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { Button, Input, Label } from "@common/components";
|
||||
|
||||
import { TermsApi } from "../api/terms-service";
|
||||
import type { Terms } from "../model/terms-interface";
|
||||
import { columns } from "../model/terms-table-columns";
|
||||
import type { TermsParams, Terms } from "../model/terms-interface";
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function TermsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [trmsSeCd, setTrmsSeCd] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<TermsParams>({
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [trmsSeCd, setTrmsSeCd] = useState(params.trmsSeCd);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.TERMS, params],
|
||||
queryFn: () =>
|
||||
TermsApi.getTermsList({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keyword: params.keyword,
|
||||
trmsSeCd: params.trmsSeCd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword,
|
||||
trmsSeCd,
|
||||
});
|
||||
setParams({ keyword, trmsSeCd, page: 0 });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeyword("");
|
||||
setTrmsSeCd("");
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keyword: "",
|
||||
trmsSeCd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Terms) => {
|
||||
@@ -72,7 +79,7 @@ function TermsPage() {
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
},
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import { useDataTable, useSearchParamsState } from "@/shared/lib";
|
||||
import { InputDateRangePicker } from "@/shared/ui/input-date-range-picker";
|
||||
import { CodeApi } from "@/entities/code";
|
||||
import {
|
||||
UserApi,
|
||||
type UserListResponse,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import { UserApi, type UserListResponse } from "@/entities/user";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -35,34 +35,39 @@ type DateRange = {
|
||||
to?: Date | null;
|
||||
};
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
page: 0,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
function parseDateRange(frDateYmd: string, toDateYmd: string): DateRange {
|
||||
return {
|
||||
from: frDateYmd ? dayjs(frDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
to: toDateYmd ? dayjs(toDateYmd, "YYYYMMDD").toDate() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function UsersPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const { params, setParams, resetParams } =
|
||||
useSearchParamsState(DEFAULT_PARAMS);
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
const [keywordType, setKeywordType] = useState("all");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [userStateCode, setUserStateCode] = useState("");
|
||||
const [aprvYn, setAprvYn] = useState("");
|
||||
const [grpId, setGrpId] = useState("");
|
||||
|
||||
const [searchParams, setSearchParams] = useState<UserSearchParams>({
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
sort: "regDt,desc",
|
||||
});
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() =>
|
||||
parseDateRange(params.frDateYmd, params.toDateYmd),
|
||||
);
|
||||
const [keywordType, setKeywordType] = useState(params.keywordType);
|
||||
const [keyword, setKeyword] = useState(params.keyword);
|
||||
const [userStateCode, setUserStateCode] = useState(params.userStateCode);
|
||||
const [aprvYn, setAprvYn] = useState(params.aprvYn);
|
||||
const [grpId, setGrpId] = useState(params.grpId);
|
||||
|
||||
const { data: userSttsStats } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, "userStatusStatistics"],
|
||||
@@ -70,13 +75,21 @@ function UsersPage() {
|
||||
});
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, searchParams, pagination],
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, params],
|
||||
queryFn: () =>
|
||||
UserApi.findPageUser({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
keywordType: params.keywordType,
|
||||
keyword: params.keyword,
|
||||
userStateCode: params.userStateCode,
|
||||
aprvYn: params.aprvYn,
|
||||
grpId: params.grpId,
|
||||
frDateYmd: params.frDateYmd,
|
||||
toDateYmd: params.toDateYmd,
|
||||
sort: params.sort,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const { data: userStateCodes } = useQuery({
|
||||
@@ -84,6 +97,16 @@ function UsersPage() {
|
||||
queryFn: () => CodeApi.getCode("USERSTTS"),
|
||||
});
|
||||
|
||||
const pagination: PaginationState = {
|
||||
pageIndex: params.page,
|
||||
pageSize: params.size,
|
||||
};
|
||||
|
||||
const handlePaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
||||
const next = typeof updater === "function" ? updater(pagination) : updater;
|
||||
setParams({ page: next.pageIndex, size: next.pageSize });
|
||||
};
|
||||
|
||||
const handleRowClick = (row: UserListResponse) => {
|
||||
router.push(`/users/${row.userUnqId}`);
|
||||
};
|
||||
@@ -97,7 +120,7 @@ function UsersPage() {
|
||||
},
|
||||
manualPagination: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
},
|
||||
dataTableOptions: {
|
||||
onRowClick: handleRowClick,
|
||||
@@ -105,14 +128,9 @@ function UsersPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
setParams({
|
||||
keywordType,
|
||||
keyword,
|
||||
userStateCode,
|
||||
@@ -122,6 +140,7 @@ function UsersPage() {
|
||||
? dayjs(dateRange.from).format("YYYYMMDD")
|
||||
: "",
|
||||
toDateYmd: dateRange?.to ? dayjs(dateRange.to).format("YYYYMMDD") : "",
|
||||
page: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -131,20 +150,12 @@ function UsersPage() {
|
||||
setUserStateCode("");
|
||||
setAprvYn("");
|
||||
setGrpId("");
|
||||
setPresetDateRange("");
|
||||
setDateRange({
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType: "all",
|
||||
keyword: "",
|
||||
userStateCode: "",
|
||||
aprvYn: "",
|
||||
grpId: "",
|
||||
frDateYmd: "",
|
||||
toDateYmd: "",
|
||||
});
|
||||
resetParams();
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
@@ -351,12 +362,9 @@ function UsersPage() {
|
||||
<h3 className="text-2xl font-medium text-label">회원 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
defaultValue="regDt,desc"
|
||||
value={params.sort}
|
||||
onValueChange={(value) => {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sort: value,
|
||||
});
|
||||
setParams({ sort: value });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { getCookie, removeCookie, setCookie } from "./cookie";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useDataTable } from "./use-data-table";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { useMutationWrapper } from "./use-mutation-wrapper";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
export { useSearchParamsState } from "./use-search-params-state";
|
||||
export { useSelectPrompt } from "./use-select-prompt";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* 조회 조건을 URL searchParams와 동기화하는 훅.
|
||||
*
|
||||
* - URL이 단일 소스 오브 트루스(SSOT)이며, 새로고침·뒤로가기 시에도 조건이 유지된다.
|
||||
* - 기본값과 동일한 값은 URL에서 제거해 쿼리스트링을 깨끗하게 유지한다.
|
||||
* - `window.history.replaceState` 기반 shallow 갱신으로 서버 라운드트립 없이 동작한다.
|
||||
* - 숫자 기본값이 주어진 키는 자동으로 `Number` 파싱한다.
|
||||
*
|
||||
* @example
|
||||
* const { params, setParams, resetParams } = useSearchParamsState({
|
||||
* keyword: "",
|
||||
* page: 0,
|
||||
* size: 10,
|
||||
* });
|
||||
*/
|
||||
export function useSearchParamsState<T extends Record<string, ParamValue>>(
|
||||
defaults: T,
|
||||
): UseSearchParamsStateReturn<T> {
|
||||
const searchParams = useSearchParams();
|
||||
const defaultsRef = useRef(defaults);
|
||||
|
||||
const params = useMemo(
|
||||
() => parseParams(searchParams, defaultsRef.current),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const setParams = useCallback((partial: Partial<T>) => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
if (value === undefined || value === defaultsRef.current[key]) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.set(key, String(value));
|
||||
}
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
const resetParams = useCallback(() => {
|
||||
const next = new URLSearchParams(window.location.search);
|
||||
for (const key of Object.keys(defaultsRef.current)) {
|
||||
next.delete(key);
|
||||
}
|
||||
replaceUrl(next);
|
||||
}, []);
|
||||
|
||||
return { params, setParams, resetParams };
|
||||
}
|
||||
|
||||
function parseParams<T extends Record<string, ParamValue>>(
|
||||
searchParams: ReturnType<typeof useSearchParams>,
|
||||
defaults: T,
|
||||
): T {
|
||||
const result = { ...defaults };
|
||||
|
||||
for (const key of Object.keys(defaults) as (keyof T)[]) {
|
||||
const raw = searchParams?.get(key as string) ?? null;
|
||||
if (raw === null) continue;
|
||||
|
||||
if (typeof defaults[key] === "number") {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isNaN(parsed)) result[key] = parsed as T[keyof T];
|
||||
} else {
|
||||
result[key] = raw as T[keyof T];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceUrl(searchParams: URLSearchParams) {
|
||||
const qs = searchParams.toString();
|
||||
const url = qs
|
||||
? `${window.location.pathname}?${qs}`
|
||||
: window.location.pathname;
|
||||
window.history.replaceState(null, "", url);
|
||||
}
|
||||
|
||||
type ParamValue = string | number | null;
|
||||
|
||||
interface UseSearchParamsStateReturn<T> {
|
||||
params: T;
|
||||
setParams: (partial: Partial<T>) => void;
|
||||
resetParams: () => void;
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { toast } from "sonner";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { useClipboard } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { deleteReasonMessages } from "@/entities/post";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useMenuStore, useSelectPrompt, useUserStore } from "@/shared/lib";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { deleteReasonMessages } from "@/entities/post";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
@@ -21,11 +23,14 @@ const selectData = Object.entries(deleteReasonMessages).map(
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const { copyText, copied } = useClipboard();
|
||||
const { prompt } = useSelectPrompt(selectData);
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const postPageLink = `/bbs/${bbsInfo?.bbsTypeCd.toLowerCase()}/${bbsInfo?.bbsId}`;
|
||||
const from = searchParams?.get("from");
|
||||
const backLink = from ?? postPageLink;
|
||||
|
||||
const onDelete = async () => {
|
||||
const { resolved, value } = await prompt("삭제 사유를 입력해주세요.");
|
||||
@@ -65,7 +70,7 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.push(postPageLink)}
|
||||
onClick={() => router.push(backLink)}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// 테스트 순차 실행 보장 (전 단계 상태가 영향을 줄 수 있으므로 serial 모드로 설정)
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
const adminId = process.env.TEST_ADMIN_ID || "admin1gmail";
|
||||
const adminPassword = process.env.TEST_ADMIN_PW || "test1234!";
|
||||
|
||||
const DEVICE_PC = "device-pc-chrome-001";
|
||||
const DEVICE_MOBILE = "device-mobile-safari-002";
|
||||
const DEVICE_TABLET = "device-tablet-firefox-003";
|
||||
|
||||
// 디바이스별로 완전히 분리된 HTTP 요청 컨텍스트를 생성하여 세션 격리를 모사하는 헬퍼 함수
|
||||
async function createDeviceContext(playwrightRequest: any, deviceId: string) {
|
||||
return await playwrightRequest.newContext({
|
||||
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000",
|
||||
extraHTTPHeaders: {
|
||||
"X-Device-Id": deviceId,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 1초 이상의 간격을 두어 토큰 발급/갱신 시 만료 시각(초)이 다르게 생성되도록 유도하는 지연 함수
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
|
||||
// 응답의 Set-Cookie 헤더 배열에서 특정 쿠키 값을 추출하는 헬퍼 함수
|
||||
function getCookieValue(
|
||||
headers: { name: string; value: string }[],
|
||||
name: string,
|
||||
): string | null {
|
||||
const cookieHeaders = headers.filter(
|
||||
(h) => h.name.toLowerCase() === "set-cookie",
|
||||
);
|
||||
for (const header of cookieHeaders) {
|
||||
const parts = header.value.split(";")[0].split("=");
|
||||
if (parts[0].trim() === name) {
|
||||
return parts[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 응답의 Set-Cookie 헤더에서 특정 쿠키의 속성(httpOnly, sameSite 등) 존재 유무를 확인하는 헬퍼 함수
|
||||
function hasCookieAttribute(
|
||||
headers: { name: string; value: string }[],
|
||||
name: string,
|
||||
attribute: string,
|
||||
): boolean {
|
||||
const cookieHeaders = headers.filter(
|
||||
(h) => h.name.toLowerCase() === "set-cookie",
|
||||
);
|
||||
for (const header of cookieHeaders) {
|
||||
const parts = header.value.split(";");
|
||||
const cookieName = parts[0].split("=")[0].trim();
|
||||
if (cookieName === name) {
|
||||
return parts
|
||||
.slice(1)
|
||||
.some((p) => p.trim().toLowerCase() === attribute.toLowerCase());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
test.describe("다중 디바이스 인증 & Refresh Token Rotation (RTR) 테스트", () => {
|
||||
|
||||
test("1. 로그인 성공 시 httpOnly refresh-token 쿠키 설정", async ({ request }) => {
|
||||
const response = await request.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
headers: { "X-Device-Id": DEVICE_PC },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const headers = response.headersArray();
|
||||
const refreshToken = getCookieValue(headers, "refresh-token");
|
||||
expect(refreshToken).not.toBeNull();
|
||||
expect(refreshToken).not.toBe("");
|
||||
|
||||
// httpOnly 속성이 설정되었는지 검증
|
||||
const isHttpOnly = hasCookieAttribute(headers, "refresh-token", "httponly");
|
||||
expect(isHttpOnly).toBe(true);
|
||||
});
|
||||
|
||||
test("2. PC/모바일/태블릿 독립 refresh-token 발급", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const mobileContext = await createDeviceContext(playwright.request, DEVICE_MOBILE);
|
||||
const tabletContext = await createDeviceContext(playwright.request, DEVICE_TABLET);
|
||||
|
||||
const pcRes = await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const pcToken = getCookieValue(pcRes.headersArray(), "refresh-token");
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
const mobileRes = await mobileContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const mobileToken = getCookieValue(mobileRes.headersArray(), "refresh-token");
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
const tabletRes = await tabletContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const tabletToken = getCookieValue(tabletRes.headersArray(), "refresh-token");
|
||||
|
||||
expect(pcToken).not.toBeNull();
|
||||
expect(mobileToken).not.toBeNull();
|
||||
expect(tabletToken).not.toBeNull();
|
||||
|
||||
// 다중 세션 환경에서는 디바이스마다 리프레시 토큰이 고유해야 함
|
||||
expect(pcToken).not.toBe(mobileToken);
|
||||
expect(pcToken).not.toBe(tabletToken);
|
||||
expect(mobileToken).not.toBe(tabletToken);
|
||||
|
||||
await pcContext.dispose();
|
||||
await mobileContext.dispose();
|
||||
await tabletContext.dispose();
|
||||
});
|
||||
|
||||
test("3. Silent Refresh RTR - 새 토큰 교체", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
|
||||
// 로그인으로 최초 토큰 발급
|
||||
const loginRes = await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const firstToken = getCookieValue(loginRes.headersArray(), "refresh-token");
|
||||
expect(firstToken).not.toBeNull();
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
// 갱신 시도 (RTR 동작)
|
||||
const refreshRes = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(refreshRes.status()).toBe(200);
|
||||
|
||||
const secondToken = getCookieValue(refreshRes.headersArray(), "refresh-token");
|
||||
expect(secondToken).not.toBeNull();
|
||||
|
||||
// 이전 토큰과 달라져야 함
|
||||
expect(secondToken).not.toBe(firstToken);
|
||||
|
||||
await pcContext.dispose();
|
||||
});
|
||||
|
||||
test("4. PC RTR 갱신 후 모바일 세션 독립 유지", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const mobileContext = await createDeviceContext(playwright.request, DEVICE_MOBILE);
|
||||
|
||||
await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
await mobileContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
// PC 토큰을 RTR 갱신함
|
||||
const pcRefresh = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(pcRefresh.status()).toBe(200);
|
||||
|
||||
// 모바일 세션은 PC와 연동되지 않고 독립적으로 정상 갱신되어야 함
|
||||
const mobileRefresh = await mobileContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(mobileRefresh.status()).toBe(200);
|
||||
|
||||
await pcContext.dispose();
|
||||
await mobileContext.dispose();
|
||||
});
|
||||
|
||||
test("5. Replay Attack - 구 토큰 재주입 → 400", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
|
||||
// 로그인 후 구 토큰 확보
|
||||
const loginRes = await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const oldToken = getCookieValue(loginRes.headersArray(), "refresh-token");
|
||||
|
||||
await delay(1000); // 1초 간격 지연
|
||||
|
||||
// 1차 갱신을 수행하여 서버(Redis)의 저장 값을 갱신
|
||||
const refreshRes1 = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(refreshRes1.status()).toBe(200);
|
||||
|
||||
// 구 토큰을 명시적으로 세팅하여 재사용 시도
|
||||
const replayContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
await replayContext.storageState({
|
||||
cookies: [
|
||||
{
|
||||
name: "refresh-token",
|
||||
value: oldToken!,
|
||||
domain: "localhost",
|
||||
path: "/",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const replayRes = await replayContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
// 사용 완료된 토큰 재주입은 거부되어야 함 (백엔드는 401 반환)
|
||||
expect(replayRes.status()).toBe(401);
|
||||
|
||||
await pcContext.dispose();
|
||||
await replayContext.dispose();
|
||||
});
|
||||
|
||||
test("6. refresh-token 없이 silent refresh → 401", async ({ playwright }) => {
|
||||
const cleanContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const res = await cleanContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
|
||||
// 토큰 부재 시 401 반환
|
||||
expect(res.status()).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Can't refresh");
|
||||
|
||||
await cleanContext.dispose();
|
||||
});
|
||||
|
||||
test("7. 잘못된 비밀번호 → 401", async ({ request }) => {
|
||||
const res = await request.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: "WrongPassword999!" },
|
||||
headers: { "X-Device-Id": DEVICE_PC },
|
||||
});
|
||||
expect(res.status()).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid Credentials");
|
||||
});
|
||||
|
||||
test("8. 존재하지 않는 ID → 401", async ({ request }) => {
|
||||
const res = await request.post("/api/proxy/user-service/login", {
|
||||
data: { userId: "non_existent_user_xyz", password: adminPassword },
|
||||
headers: { "X-Device-Id": DEVICE_PC },
|
||||
});
|
||||
expect(res.status()).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toBe("Invalid Credentials");
|
||||
});
|
||||
|
||||
test("9. 로그아웃 후 쿠키 전체 삭제", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
|
||||
// 로그아웃 호출
|
||||
const logoutRes = await pcContext.post("/api/auth/logout");
|
||||
expect(logoutRes.status()).toBe(200);
|
||||
|
||||
// 클라이언트의 인증 쿠키들이 소멸되었는지 검증
|
||||
const state = await pcContext.storageState();
|
||||
const refreshCookie = state.cookies.find((c) => c.name === "refresh-token");
|
||||
const accessCookie = state.cookies.find((c) => c.name === "access-token");
|
||||
const userCookie = state.cookies.find((c) => c.name === "user-id");
|
||||
|
||||
expect(refreshCookie).toBeUndefined();
|
||||
expect(accessCookie).toBeUndefined();
|
||||
expect(userCookie).toBeUndefined();
|
||||
|
||||
await pcContext.dispose();
|
||||
});
|
||||
|
||||
test("10. PC 로그아웃 후 모바일 세션 독립 유지", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const mobileContext = await createDeviceContext(playwright.request, DEVICE_MOBILE);
|
||||
|
||||
await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
await mobileContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
|
||||
// PC 로그아웃
|
||||
await pcContext.post("/api/auth/logout");
|
||||
|
||||
// 모바일은 PC의 로그아웃과 무관하게 독립적으로 유효하게 동작해야 함
|
||||
const mobileRefresh = await mobileContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(mobileRefresh.status()).toBe(200);
|
||||
|
||||
await pcContext.dispose();
|
||||
await mobileContext.dispose();
|
||||
});
|
||||
|
||||
test("11. UI 로그인 후 httpOnly 쿠키 E2E 확인", async ({ page }) => {
|
||||
// UI 로그인 페이지 접속
|
||||
await page.goto("/auth/login");
|
||||
|
||||
// Form 필드 입력
|
||||
await page.locator("input#id").fill(adminId);
|
||||
await page.locator("input#password").fill(adminPassword);
|
||||
|
||||
// 디바이스 ID 쿠키 설정 시뮬레이션
|
||||
await page.context().addCookies([
|
||||
{
|
||||
name: "device-id",
|
||||
value: DEVICE_PC,
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
|
||||
// 로그인 버튼 클릭
|
||||
await page.locator(".login_btn").click();
|
||||
|
||||
// 성공 시 대시보드 페이지 이동 대기
|
||||
await page.waitForURL("**/dashboard");
|
||||
|
||||
// E2E 보안 요구사항: 클라이언트 브라우저 JavaScript(document.cookie)로는 httpOnly 쿠키를 읽을 수 없어야 함
|
||||
const clientCookies = await page.evaluate(() => document.cookie);
|
||||
expect(clientCookies).not.toContain("refresh-token");
|
||||
|
||||
// 실제 브라우저의 HTTP Context 쿠키에는 저장되어 있어야 함
|
||||
const contextCookies = await page.context().cookies();
|
||||
const refreshCookie = contextCookies.find((c) => c.name === "refresh-token");
|
||||
expect(refreshCookie).toBeDefined();
|
||||
expect(refreshCookie?.httpOnly).toBe(true);
|
||||
});
|
||||
|
||||
test("12. 동일 계정 3개 브라우저 동시 로그인", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const mobileContext = await createDeviceContext(playwright.request, DEVICE_MOBILE);
|
||||
const tabletContext = await createDeviceContext(playwright.request, DEVICE_TABLET);
|
||||
|
||||
const pcRes = await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
expect(pcRes.status()).toBe(200);
|
||||
|
||||
const mobileRes = await mobileContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
expect(mobileRes.status()).toBe(200);
|
||||
|
||||
const tabletRes = await tabletContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
expect(tabletRes.status()).toBe(200);
|
||||
|
||||
// 3개 브라우저 세션 모두 정상 리프레시 수행 가능 확인
|
||||
const pcRef = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(pcRef.status()).toBe(200);
|
||||
|
||||
const mobRef = await mobileContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(mobRef.status()).toBe(200);
|
||||
|
||||
const tabRef = await tabletContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(tabRef.status()).toBe(200);
|
||||
|
||||
await pcContext.dispose();
|
||||
await mobileContext.dispose();
|
||||
await tabletContext.dispose();
|
||||
});
|
||||
|
||||
test("13. sameSite=Strict 쿠키 속성 확인", async ({ request }) => {
|
||||
const response = await request.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
headers: { "X-Device-Id": DEVICE_PC },
|
||||
});
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const headers = response.headersArray();
|
||||
const isStrict = hasCookieAttribute(headers, "refresh-token", "samesite=strict");
|
||||
expect(isStrict).toBe(true);
|
||||
});
|
||||
|
||||
test("14. 연속 3회 RTR 갱신 - 4개 고유 토큰", async ({ playwright }) => {
|
||||
const pcContext = await createDeviceContext(playwright.request, DEVICE_PC);
|
||||
const tokens: string[] = [];
|
||||
|
||||
// 로그인 (1차 토큰 발급)
|
||||
const loginRes = await pcContext.post("/api/proxy/user-service/login", {
|
||||
data: { userId: adminId, password: adminPassword },
|
||||
});
|
||||
const t1 = getCookieValue(loginRes.headersArray(), "refresh-token");
|
||||
expect(t1).not.toBeNull();
|
||||
tokens.push(t1!);
|
||||
|
||||
await delay(1000);
|
||||
|
||||
// 1차 갱신 (2차 토큰 발급)
|
||||
const r1 = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(r1.status()).toBe(200);
|
||||
const t2 = getCookieValue(r1.headersArray(), "refresh-token");
|
||||
expect(t2).not.toBeNull();
|
||||
tokens.push(t2!);
|
||||
|
||||
await delay(1000);
|
||||
|
||||
// 2차 갱신 (3차 토큰 발급)
|
||||
const r2 = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(r2.status()).toBe(200);
|
||||
const t3 = getCookieValue(r2.headersArray(), "refresh-token");
|
||||
expect(t3).not.toBeNull();
|
||||
tokens.push(t3!);
|
||||
|
||||
await delay(1000);
|
||||
|
||||
// 3차 갱신 (4차 토큰 발급)
|
||||
const r3 = await pcContext.put("/api/proxy/user-service/api/v1/users/token/refresh");
|
||||
expect(r3.status()).toBe(200);
|
||||
const t4 = getCookieValue(r3.headersArray(), "refresh-token");
|
||||
expect(t4).not.toBeNull();
|
||||
tokens.push(t4!);
|
||||
|
||||
// 4개 토큰이 모두 고유한지 검증 (Set의 중복 제거 크기로 판별)
|
||||
const uniqueTokens = new Set(tokens);
|
||||
expect(uniqueTokens.size).toBe(4);
|
||||
|
||||
await pcContext.dispose();
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,8 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:800
|
||||
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
CONTENTS_INTRO = 31
|
||||
CONTENTS_EXAMPLE = 32
|
||||
CONTENTS_INTRO = 24
|
||||
CONTENTS_EXAMPLE = 25
|
||||
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
|
||||
|
||||
@@ -365,7 +365,7 @@ export default function SimilarCaseModal({
|
||||
</tr>
|
||||
<tr>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
총대장균군수<br />(개/100ml)
|
||||
총대장균군수<br />(개/100mL)
|
||||
</th>
|
||||
<th rowSpan={2} className={thBase}>
|
||||
용도별 결합 잔류염소<br />(mg/L)
|
||||
|
||||
@@ -428,7 +428,7 @@ export default function DemandRegisterPage() {
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<FormRow label="요구수량(㎥/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} />
|
||||
</FormRow>
|
||||
<Divider />
|
||||
@@ -438,7 +438,7 @@ export default function DemandRegisterPage() {
|
||||
</FormRow>
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<FormRow label="희망단가(원/㎥)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
@@ -464,7 +464,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftLabel="총대장균군수(개/100㎖)"
|
||||
leftRequired
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function DemandStatusPage() {
|
||||
{ label: "요구시기", value: selected.detail.reqDate, required: true },
|
||||
{ label: "요구수량(㎡/d)", value: selected.detail.reqQty },
|
||||
{ label: "사용용도", value: selected.detail.usePurpose },
|
||||
{ label: "희망단가(원)", value: selected.detail.price },
|
||||
{ label: "희망단가(원/㎥)", value: selected.detail.price },
|
||||
];
|
||||
}, [selected]);
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function DemandStatusPage() {
|
||||
if (!selected) return [];
|
||||
const w = selected.detail.water;
|
||||
return [
|
||||
{ label: "총대장균군수(개/100mℓ)", value: w.coliform },
|
||||
{ label: "총대장균군수(개/100㎖)", value: w.coliform },
|
||||
{ label: "결합잔류염소(mg/L)", value: "-" },
|
||||
{ label: "탁도(NTU)", value: w.turbidity },
|
||||
|
||||
|
||||
@@ -44,17 +44,17 @@ export default function DemandRegisterPage() {
|
||||
const [coliform, setColiform] = useState<string>(""); // 총대장균군수
|
||||
const [noDetect, setNoDetect] = useState<boolean>(false);
|
||||
|
||||
const [tn, setTn] = useState<string>("");
|
||||
const [residualCl, setResidualCl] = useState<string>("");
|
||||
const [tp, setTp] = useState<string>("");
|
||||
const [turbidity, setTurbidity] = useState<string>("");
|
||||
const [ph, setPh] = useState<PHRange>({ min: "", max: "" });
|
||||
const [bod, setBod] = useState<string>("");
|
||||
const [chloride, setChloride] = useState<string>("");
|
||||
const [odor, setOdor] = useState<string>("불쾌하지 않을 것");
|
||||
const [ec, setEc] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
const [remark, setRemark] = useState<string>("");
|
||||
const [tn, setTn] = useState<string>(""); // 총질소(T-N)
|
||||
const [residualCl, setResidualCl] = useState<string>(""); // 결합잔류염소
|
||||
const [tp, setTp] = useState<string>(""); // 총인(T-P)
|
||||
const [turbidity, setTurbidity] = useState<string>(""); // 탁도
|
||||
const [ph, setPh] = useState<PHRange>({ min: "", max: "" }); // 수소이온농도(pH)
|
||||
const [bod, setBod] = useState<string>(""); // 생물학적산소요구량(BOD)
|
||||
const [chloride, setChloride] = useState<string>(""); // 염화물
|
||||
const [odor, setOdor] = useState<string>("불쾌하지 않을 것"); // 냄새
|
||||
const [ec, setEc] = useState<string>(""); // 전기전도도
|
||||
const [color, setColor] = useState<string>(""); // 색도
|
||||
const [remark, setRemark] = useState<string>(""); // 비고
|
||||
|
||||
// 공급처 후보지 선택(필수아님)
|
||||
const [cand1, setCand1] = useState("");
|
||||
@@ -373,7 +373,8 @@ export default function DemandRegisterPage() {
|
||||
switch (value) {
|
||||
// 청소화장실용도
|
||||
case '01':
|
||||
setTp('0.2');
|
||||
setNoDetect(true); // 총대장균군수 불검출
|
||||
setResidualCl('0.2'); // 잔류염소
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
@@ -381,23 +382,35 @@ export default function DemandRegisterPage() {
|
||||
break;
|
||||
// 세척살수용도
|
||||
case '02':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setColiform('1000');
|
||||
setTurbidity('2'); // 탁도
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 조경용도
|
||||
case '03':
|
||||
setTp('0.2');
|
||||
setColiform('1000');
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('250');
|
||||
setChloride('250'); // 염화물
|
||||
break;
|
||||
// 지하수
|
||||
// 친수용도
|
||||
case '04':
|
||||
setNoDetect(true); // 총대장균군수 불검출
|
||||
setResidualCl('0.1'); // 잔류염소
|
||||
setTurbidity('2');
|
||||
setBod('3');
|
||||
setColor('10');
|
||||
setTn('10'); // 총질소
|
||||
setTp('0.5'); // 총인
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setChloride('0.5');
|
||||
break;
|
||||
// 지하수충전
|
||||
case '05':
|
||||
break;
|
||||
// 농업용도
|
||||
// 농업용수
|
||||
case '06':
|
||||
setTurbidity('5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
@@ -405,16 +418,18 @@ export default function DemandRegisterPage() {
|
||||
setEc('2000');
|
||||
setFarmText("이하 (** 간접:2000,직접:700 )");
|
||||
break;
|
||||
// 하천 등 유지보수
|
||||
// 하천 등 유지용수
|
||||
case '07':
|
||||
setColiform('1000');
|
||||
setTn('10');
|
||||
setBod('5');
|
||||
setTn('20');
|
||||
setTp('0.5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('0.5');
|
||||
break;
|
||||
// 공업
|
||||
// 공업용수
|
||||
case '08':
|
||||
setBod('20');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
break;
|
||||
default:
|
||||
@@ -467,7 +482,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<FormRow label="요구수량(㎥/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
@@ -490,7 +505,7 @@ export default function DemandRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<FormRow label="희망단가(원/㎥)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
@@ -519,7 +534,7 @@ export default function DemandRegisterPage() {
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
leftLabel="총대장균군수(개/100㎖)"
|
||||
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -30,6 +30,19 @@ const formatValue = (value: string | number | null | undefined) => {
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatNumberWithComma = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
|
||||
const normalized =
|
||||
typeof value === "string" ? Number(value.replace(/,/g, "")) : Number(value);
|
||||
|
||||
if (Number.isNaN(normalized)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return normalized.toLocaleString("ko-KR");
|
||||
};
|
||||
|
||||
const formatBelow = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (Number(value) === -1) return "불검출";
|
||||
@@ -234,9 +247,9 @@ export default function DemandStatusPage() {
|
||||
|
||||
return [
|
||||
{ label: "요구시기", key: "dmndTmng", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
|
||||
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatValue(selectedSdmInfoStatus.dmndQty) },
|
||||
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatNumberWithComma(selectedSdmInfoStatus.dmndQty) },
|
||||
{ label: "사용용도", key: "useUsgCd", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
|
||||
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
|
||||
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatNumberWithComma(selectedSdmInfoStatus.wntUntprc) },
|
||||
];
|
||||
}, [selectedSdmInfoStatus]);
|
||||
|
||||
@@ -244,7 +257,7 @@ export default function DemandStatusPage() {
|
||||
if (!selectedSdmInfoStatus) return [];
|
||||
|
||||
return [
|
||||
{ label: "총대장균군수(개/100mL)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
|
||||
{ label: "총대장균군수(개/100㎖)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
|
||||
{ label: "결합잔류염소(mg/L)", key:"combResdch", value: formatBelow(selectedSdmInfoStatus.combResdch) },
|
||||
{ label: "탁도(NTU)", key:"trbt", value: formatBelow(selectedSdmInfoStatus.trbt) },
|
||||
|
||||
@@ -520,7 +533,7 @@ export default function DemandStatusPage() {
|
||||
|
||||
{!selectedSdmInfoStatus ? (
|
||||
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
|
||||
상세 정보를 표시할 신청건이 없습니다.
|
||||
조회된 결과가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 border border-neutral-200">
|
||||
|
||||
@@ -17,7 +17,7 @@ type IntroCardItem = {
|
||||
|
||||
const INTRO_CARDS: IntroCardItem[] = [
|
||||
{
|
||||
title: "물 재이용 법",
|
||||
title: "물재이용법",
|
||||
desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.",
|
||||
image: card01,
|
||||
href: "/intro#law-plan",
|
||||
|
||||
@@ -770,7 +770,7 @@ export const SdmReducer = (set: any): SdmActions => {
|
||||
"/cms-service/api/v1/lwpo-cds/?lwpoCdNm=&sort=regDt,desc&upCd=WR018&page=0&size=100"
|
||||
);
|
||||
|
||||
const comUseTypes = upCd.content.filter(el=>el.lwpoCdNm != '친수용수').map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
|
||||
const comUseTypes = upCd.content.map((el: any) => ({ text: el.lwpoCdNm, val: el.lwpoCd }));
|
||||
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
|
||||
@@ -83,14 +83,14 @@ const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
sewage: [
|
||||
{
|
||||
id: "sewage-1",
|
||||
title: "구리시 하수 재이용",
|
||||
title: "구리시 하수처리수 재이용",
|
||||
desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화",
|
||||
imageSrc: "/images/landing/business/sewage/guri_01.png",
|
||||
href: "/example#example-sewage-guri",
|
||||
},
|
||||
{
|
||||
id: "sewage-2",
|
||||
title: "부천시 하수재이용",
|
||||
title: "부천시 하수처리수 재이용",
|
||||
desc: "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공",
|
||||
imageSrc: "/images/landing/business/sewage/bucheon_01.png",
|
||||
href: "/example#example-sewage-bucheon",
|
||||
@@ -104,42 +104,42 @@ const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
},
|
||||
{
|
||||
id: "sewage-4",
|
||||
title: "파주시 하수 재이용 시설",
|
||||
title: "파주시 하수처리수 재이용",
|
||||
desc: "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)",
|
||||
imageSrc: "/images/landing/business/sewage/paju_01.png",
|
||||
href: "/example#example-sewage-paju",
|
||||
},
|
||||
{
|
||||
id: "sewage-5",
|
||||
title: "포항시 하수 재이용 시설",
|
||||
title: "포항시 하수처리수 재이용",
|
||||
desc: "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등), 포항하수처리장 하수처리수를 재처리하여 공업용수로 공급",
|
||||
imageSrc: "/images/landing/business/sewage/pohang_01.png",
|
||||
href: "/example#example-sewage-pohang",
|
||||
},
|
||||
{
|
||||
id: "sewage-6",
|
||||
title: "울산광역시 하수 재이용 시설",
|
||||
title: "울산광역시 하수처리수 재이용",
|
||||
desc: "18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급",
|
||||
imageSrc: "/images/landing/business/sewage/ulsan_01.png",
|
||||
href: "/example#example-sewage-ulsan",
|
||||
},
|
||||
{
|
||||
id: "sewage-7",
|
||||
title: "여수시 하수 재이용 시설",
|
||||
title: "여수시 하수처리수 재이용",
|
||||
desc: "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)",
|
||||
imageSrc: "/images/landing/business/sewage/yeosu_01.png",
|
||||
href: "/example#example-sewage-yeosu",
|
||||
},
|
||||
{
|
||||
id: "sewage-8",
|
||||
title: "보령시 하수 재이용 시설",
|
||||
title: "보령시 하수처리수 재이용",
|
||||
desc: "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진, 하수처리수 재이용을 통한 수자원 절약 및 지역 물 부족 해소",
|
||||
imageSrc: "/images/landing/business/sewage/boryeong_01.png",
|
||||
href: "/example#example-sewage-boryeong",
|
||||
},
|
||||
{
|
||||
id: "sewage-9",
|
||||
title: "아산시 하수 재이용 시설",
|
||||
title: "아산시 하수처리수 재이용",
|
||||
desc: "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모",
|
||||
imageSrc: "/images/landing/business/sewage/asan_01.png",
|
||||
href: "/example#example-sewage-asan",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
// },
|
||||
// {
|
||||
// name: "money",
|
||||
// label: "희망단가(원)",
|
||||
// label: "희망단가(원/㎥)",
|
||||
// formIndex: 0,
|
||||
// required: true,
|
||||
// component: (field) => (
|
||||
@@ -375,7 +375,7 @@
|
||||
// render={({ field }) =>
|
||||
// name === "money" ? (
|
||||
// <div>
|
||||
// <Label className="text-black">희망단가(원)*</Label>
|
||||
// <Label className="text-black">희망단가(원/㎥)*</Label>
|
||||
// {component(field)}
|
||||
// </div>
|
||||
// ) : (
|
||||
@@ -411,7 +411,7 @@
|
||||
// return (
|
||||
// <FormUi
|
||||
// label={
|
||||
// !isDisabled ? `총대장균군수(개/mg)*` : "총대장균군수(개/mg)"
|
||||
// !isDisabled ? `총대장균군수(개/100㎖)*` : "총대장균군수(개/100㎖)"
|
||||
// }
|
||||
// >
|
||||
// <div className="flex items-center gap-2">
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 60 KiB |
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
onSelectValue={(val) => setFlowPanel(val.toString())}
|
||||
className="mx-3 w-[150px]"
|
||||
label=""
|
||||
selectClass="w-full p-1 border rounded bg-white"
|
||||
selectClass="w-full rounded border border-input bg-form p-1"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
@@ -262,7 +262,7 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
@@ -275,4 +275,4 @@ export default function FacilityTermModalHeader<T extends { msrDt: string }>({
|
||||
</div>
|
||||
</ListDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
|
||||
return (
|
||||
<ListDiv className="!h-[100px]">
|
||||
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0r">
|
||||
<div className="card !m-[1.2rem] max-h-[40rem] overflow-auto !p-0">
|
||||
<Table>
|
||||
<thead>
|
||||
<TableRow>
|
||||
@@ -237,7 +237,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
onSelectValue={(val) => setFlowPanel(val.toString())}
|
||||
className="mx-3 w-[150px]"
|
||||
label=""
|
||||
selectClass="w-full p-1 border rounded bg-white"
|
||||
selectClass="w-full rounded border border-input bg-form p-1"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -251,7 +251,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
</div>
|
||||
@@ -261,7 +261,7 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
</div>
|
||||
@@ -273,4 +273,4 @@ export default function FacilityTermModalHeaderMonth<T extends { msrDt: string }
|
||||
</div>
|
||||
</ListDiv>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,55 +5,55 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 'A' ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : layoutNum === 'R' ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'유역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -99,22 +99,22 @@ export default function SearchInputText({label=undefined, setValue=()=>{}, sidoS
|
||||
|
||||
return (
|
||||
<div className={'relative'} ref={wrapperRef}>
|
||||
{label && <Label className="w-20 text-xl text-gray-600 justify-self-center">{label}</Label>}
|
||||
{label && <Label className="w-20 justify-self-center text-xl text-label">{label}</Label>}
|
||||
<input
|
||||
type="text"
|
||||
value={textValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={onInputFocus}
|
||||
className="w-[200px] ml-6 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="ml-6 w-[200px] rounded-md border border-input bg-form px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
placeholder={`${label}을(를) 입력하세요`}
|
||||
/>
|
||||
{isOpen && processFacilityList.length > 0 && (
|
||||
<div className="absolute z-10 w-[300px] px-2 py-1 !mt-5 ml-20 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
|
||||
<div className="absolute z-10 ml-20 !mt-5 h-[90px] w-[300px] overflow-auto rounded-md border border-border bg-popover px-2 py-1 text-popover-foreground shadow-lg">
|
||||
<div className="max-h-72">
|
||||
{processFacilityList.map((item : facilityListProps, index : number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-full text-left pl-1 pr-10 py-2 hover:bg-gray-100 cursor-pointer overflow-auto"
|
||||
className="w-full cursor-pointer overflow-auto py-2 pl-1 pr-10 text-left hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={() => handleSuggestionClick(item.bscFctCd, item.bscFctNm, Number(item.fctCpc), item.trtFrfNm, item.efflWqltStndrdNm)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -17,7 +17,7 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
|
||||
<div className={"mt-"+divHeight}>
|
||||
{commentArray.map((item, index) => (
|
||||
<React.Fragment key={"comment" + index}>
|
||||
<span className="text-2xl font-bold text-gray-500">
|
||||
<span className="text-2xl font-bold text-muted-foreground">
|
||||
{commentStartPoint} {item}
|
||||
</span>
|
||||
<br/>
|
||||
@@ -28,4 +28,4 @@ function UnderComment({commentStartPoint="☞", commentArray=[], divHeight="14"}
|
||||
}
|
||||
}
|
||||
|
||||
export default UnderComment;
|
||||
export default UnderComment;
|
||||
|
||||
@@ -127,15 +127,18 @@ const Map = () => {
|
||||
}
|
||||
const el = overlayContainerRef.current;
|
||||
el.className =
|
||||
"ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] ";
|
||||
"ol-custom-overlay shadow-xl rounded-2xl backdrop-blur p-3 min-w-[220px] max-w-[350px]";
|
||||
el.style.backgroundColor = "color-mix(in srgb, var(--popover) 95%, transparent)";
|
||||
el.style.color = "var(--popover-foreground)";
|
||||
el.style.border = "1px solid var(--border)";
|
||||
el.innerHTML = `
|
||||
<div class="font-semibold text-lg text-gray-800 mb-2 text-center bg-green-400">${title}</div>
|
||||
<table class="w-full text-base text-gray-800">
|
||||
<div class="mb-2 rounded-md px-2 py-1 text-center text-lg font-semibold" style="background-color: var(--secondary); color: var(--secondary-foreground);">${title}</div>
|
||||
<table class="w-full text-base" style="color: var(--foreground);">
|
||||
${Object.entries(props)
|
||||
.map(
|
||||
([k, v]) => `
|
||||
<tr>
|
||||
<td class="py-1 pr-2 text-gray-500 whitespace-nowrap">${k}</td>
|
||||
<td class="whitespace-nowrap py-1 pr-2" style="color: var(--muted-foreground);">${k}</td>
|
||||
<td class="py-1 font-medium">${safe(v)}</td>
|
||||
</tr>`
|
||||
)
|
||||
|
||||
@@ -54,23 +54,23 @@ export default function DrainageEquipment({searchYear = '', fcltyList = []}: Dra
|
||||
const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
|
||||
return (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'part1' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
|
||||
head.id === 'part2' ? <TableHead rowSpan={3} className='w-60' key={head.id}>구분</TableHead> :
|
||||
head.id == 'nowSum' ? <TableHead rowSpan={2} colSpan={3} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'현재\n(기준년도)'}</TableHead> :
|
||||
head.id == 'stepOneSum' ? <TableHead rowSpan={1} colSpan={12} key={head.id} style={{ whiteSpace: 'pre-line' }}>{'변경(금회계획)'}</TableHead> :
|
||||
head.id == 'etc' ? <TableHead rowSpan={3} key={head.id}>비고</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'stepOneSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>1단계</TableHead> :
|
||||
head.id === 'stepTwoSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>2단계</TableHead> :
|
||||
head.id === 'stepThreeSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>3단계</TableHead> :
|
||||
head.id === 'stepFourSum' ? <TableHead rowSpan={1} colSpan={3} key={head.id}>4단계</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function DashBoardChartModal({item='', period='', title='',modalD
|
||||
if (label) {
|
||||
config[label] = {
|
||||
label: label,
|
||||
color : "hsl(var(--chart-1))"
|
||||
color : "var(--chart-1)"
|
||||
};
|
||||
}
|
||||
return config;
|
||||
|
||||
@@ -32,12 +32,12 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
|
||||
return ( <>
|
||||
<TableRow>{
|
||||
upHeadList.map((head: TableUpperProps) =>
|
||||
(!head.upSequnce?<TableHead rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
|
||||
(!head.upSequnce?<TableHead className="fo-facility-info-header-surface" rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe'&& head.upSequnce=== 1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={1} colSpan={7} style={{ whiteSpace: 'pre-line' }}>{'시설계획\n(금회)'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{upHeadList.map((head: TableUpperProps, index: number) =>
|
||||
(head.upName === 'upChangeRe'?<TableHead key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe'?<TableHead className="fo-facility-info-header-surface" key={head.id} style={{ whiteSpace: 'pre-line' }}>{index===0?head.title:head.title+'\n('+planYearList[index-1]+')'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
</> )
|
||||
}
|
||||
@@ -52,20 +52,20 @@ const headMakeColSpanPlanFclty = (upHeadList: TableUpperProps[], planYearList: s
|
||||
const headMakeColSpanOperationStatus = (upHeadList: TableUpperProps[], searchYear: string) => {
|
||||
return ( <>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
<TableHead className="fo-facility-info-header-surface" rowSpan={1} colSpan={17} style={{ whiteSpace: 'pre-line' }}>{searchYear+'년도 운영실태'}</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>{
|
||||
upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
|
||||
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe1'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} rowSpan={2}colSpan={2} style={{ whiteSpace: 'pre-line' }}>{head.title}</TableHead>:
|
||||
!head.upSequnce?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} rowSpan={2}key={head.id}>{head.title}</TableHead>:
|
||||
head.upName === 'upChangeRe2'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'연간 운영실태'}</TableHead>:
|
||||
head.upName === 'upChangeRe3'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'하절기 운영실태\n(6~8월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe4'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={4} style={{ whiteSpace: 'pre-line' }}>{'동절기 운영실태\n(1~2월)'}</TableHead>:
|
||||
head.upName === 'upChangeRe5'&&head.upSequnce===1?<TableHead className="fo-facility-info-header-surface" key={head.id} colSpan={3} style={{ whiteSpace: 'pre-line' }}>{'지하수 유입량 추정'}</TableHead>:null))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
|
||||
(head.upName === 'upChangeRe2'||head.upName === 'upChangeRe3'||head.upName === 'upChangeRe4'||head.upName === 'upChangeRe5'?<TableHead className="fo-facility-info-header-surface" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>:null))}
|
||||
</TableRow>
|
||||
</> )
|
||||
}
|
||||
@@ -239,18 +239,18 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
:((index === 6 || index === 7 || index === 8) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree' || head.id === 'stepFour'))?
|
||||
null
|
||||
:((index === 5) && (head.id === 'provisionalApproval' || head.id === 'stepTwo'))?
|
||||
<TableCell colSpan={3} className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
<TableCell colSpan={3} className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:((index === 5) && (head.id === 'now' || head.id === 'stepOne' || head.id === 'stepThree'|| head.id === 'stepFour'))?
|
||||
null
|
||||
:(index < 5 || index > 5)&& (index!= 4) && (head.id ==='provisionalApproval'||head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour')
|
||||
?<TableCell className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:(head.id === "provisionalApproval" && index === 4)
|
||||
?<TableCell colSpan={6} className="text-right border border-[var(--border)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
|
||||
?<TableCell colSpan={6} className="text-right border border-border text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{roundNumber(String((Number(waterUsage1)+Number(waterUsage2)+Number(waterUsage3))/3), 0)}</TableCell>
|
||||
:((head.id ==='now'||head.id ==='stepOne'||head.id ==='stepTwo'||head.id ==='stepThree'||head.id ==='stepFour') )
|
||||
?null
|
||||
:(head.id === 'item' && index != 5)?
|
||||
<TableCell className="text-center text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:<TableCell className="text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)]" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
<TableCell className="text-center text-label" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
:<TableCell className="fo-facility-info-header-surface text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -278,15 +278,15 @@ export default function ProcessFacilitySearch({searchYear = '', fcltyList = []}:
|
||||
(head.id === 'rdiRain'&&index === 0)?<TableCell rowSpan={7} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el['rdiRain']}</TableCell>:
|
||||
(head.id === 'rdiRain'&&index === 7)?<TableCell rowSpan={6} className="text-right" style={{ whiteSpace: 'pre-line' }} key={head.id+index}></TableCell>:
|
||||
(head.id === 'waterQlty')?<TableCell className="text-center" style={{ whiteSpace: 'pre-line' }} key={head.id+index}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="bg-green-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-50 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="bg-yellow-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="bg-green-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="bg-blue-100 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="bg-yellow-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="bg-green-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="bg-blue-200 text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'yearAvg' || head.id === 'yearMax' || head.id === 'yearMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-year text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'year', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'summerAll' || head.id === 'summerThree' || head.id === 'summerRain') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-summer text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'summer', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=0 && index<=4))?<TableCell className="fo-facility-info-flow-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('flowRate', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=5 && index<=10))?<TableCell className="fo-facility-info-water-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('waterQlty', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
((head.id === 'winterAll' || head.id === 'winterSunnyAvg' || head.id === 'winterSunnyMax' || head.id === 'winterSunnyMin') && (index>=11 && index<=12))?<TableCell className="fo-facility-info-phosphorus-winter text-right cursor-pointer" style={{ whiteSpace: 'pre-line' }} key={head.id+index} onClick={()=>{fncLoadModal('chongin', 'winter', el['waterQlty'] as string);}}>{el[head.id]}</TableCell>:
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
@@ -60,7 +60,14 @@ export default function DashBoardChart({searchYear = '', fcltyList = []}: facili
|
||||
/**
|
||||
* 차트 Config
|
||||
*/
|
||||
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#00b50c", "#D4A5A5"];
|
||||
const colors = [
|
||||
"var(--chart-5)",
|
||||
"var(--chart-2)",
|
||||
"var(--chart-1)",
|
||||
"var(--chart-3)",
|
||||
"var(--primary)",
|
||||
"var(--chart-6)",
|
||||
];
|
||||
|
||||
//유입하수량-강수량
|
||||
const chartConfig_inflowRain = {
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function RainManageTab({ searchYear = '', fcltyList = [] }: facil
|
||||
widthFixed={true}
|
||||
>
|
||||
</UiTable>
|
||||
<span className="text-2xl font-bold text-gray-500">☞ 일수에 강우영향일 포함됨</span>
|
||||
<span className="text-2xl font-bold text-muted-foreground">☞ 일수에 강우영향일 포함됨</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 대시보드 - 표로보기 상단구역 페에지
|
||||
* 대시보드 - 표로보기 상단구역 페이지
|
||||
*
|
||||
* - 대시보드 - 표로보기의 3개의 탭 상단에 조회년도, 행정정보, 시설명의 구역을 컴포넌트
|
||||
*/
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
mode="single"
|
||||
selected={startDate}
|
||||
onSelect={(date) => handleStartDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={startDate}
|
||||
/>
|
||||
@@ -209,7 +209,7 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
mode="single"
|
||||
selected={endDate}
|
||||
onSelect={(date) => handleEndDateSelect(date as Date)}
|
||||
className="w-full p-1 border rounded bg-white"
|
||||
className="w-full rounded border border-input bg-form p-1 text-foreground"
|
||||
disabled={(date) => !isDateInRanges(date)}
|
||||
defaultMonth={endDate}
|
||||
/>
|
||||
@@ -239,4 +239,4 @@ export default function PrecipitationModal(props: PrecipitationModalProps) {
|
||||
</ListDiv>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 0 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -45,27 +45,27 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 1 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -73,29 +73,29 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 2 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={3} style={{ whiteSpace: 'pre-line' }} key={head.id}>권역</TableHead>
|
||||
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>행정구역</TableHead>
|
||||
}else if(head.upName==="upChangeRe3"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe4"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유출</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -103,25 +103,25 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 3 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe2"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={5} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -129,23 +129,23 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 4 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe"&&head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={4} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps, idx: number) => {
|
||||
if (head.upName === 'upChangeRe') {
|
||||
if (idx == 1 || idx == 2 || idx == 3) {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
else {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
}
|
||||
})}
|
||||
@@ -153,21 +153,21 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
</>
|
||||
) : layoutNum === 5 ? ( // 총인설비 수질현황 헤더
|
||||
<>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if(!head.upName) {
|
||||
return <TableHead className="w-[180px] bg-gray-50" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
return <TableHead className="w-[180px] bg-secondary" rowSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead>
|
||||
}else if(head.upName==="upChangeRe1" && head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>유입</TableHead>
|
||||
}else if(head.upName==="upChangeRe2" && head.upSequnce===1) {
|
||||
return <TableHead className="bg-gray-50" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
return <TableHead className="bg-secondary" colSpan={2} style={{ whiteSpace: 'pre-line' }} key={head.id}>방류</TableHead>
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-1/8 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
|
||||
@@ -237,7 +237,7 @@ export default function FlowRateModal(props: { fcltyName: string; facilityCd: st
|
||||
const raw = row.infoRnDay;
|
||||
const value = typeof raw === "string" ? Number(raw.replace(/,/g, "").trim()) : Number(raw);
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return value > 3 ? "bg-slate-50" : "";
|
||||
return value > 3 ? "fo-rain-impact-row" : "";
|
||||
},
|
||||
[activeTab],
|
||||
);
|
||||
|
||||
@@ -5,77 +5,77 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
return (
|
||||
layoutNum === 0 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={2}>{'행정구역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={19}>{'시설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => (
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : layoutNum === 1 ? (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={3} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={3} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={3}>{'유역'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={2} colSpan={6}>{'증설계획'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={3} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => (
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
head.upName === 'upChangeRe4' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
null
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe' || head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' || head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableRow className="bg-gray-50">{upHeadList.map((head: TableUpperProps) =>
|
||||
<TableRow className="bg-secondary">{upHeadList.map((head: TableUpperProps) =>
|
||||
(head.id === 'trtFrfNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'laeNm' ? <TableHead className="w-1/12" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.id == 'fctCpc' || head.id == 'planInputWaterQlty' || head.id == 'designInputWaterQlty' ? <TableHead rowSpan={2} className="w-40" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> :
|
||||
head.id === 'trstOprEtpNm' ? <TableHead className="w-1/15" rowSpan={2} key={head.id}>{head.title}</TableHead> :
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-gray-50" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-gray-20" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
head.upName === 'upChangeRe1' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={3}>{'권역'}</TableHead> :
|
||||
head.upName === 'upChangeRe2' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={2}>{'환경청'}</TableHead> :
|
||||
head.upName === 'upChangeRe3' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'증설계획(m³/일)'}</TableHead> :
|
||||
head.upName === 'upChangeRe5' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={6}>{'처리인구(인)'}</TableHead> :
|
||||
head.upName === 'upChangeRe6' && head.upSequnce === 1 ? <TableHead className="w-75 bg-secondary" style={{ whiteSpace: 'pre-line' }} key={head.id} rowSpan={1} colSpan={7}>{'처리구역(k㎡)'}</TableHead> :
|
||||
!head.upName ? <TableHead rowSpan={2} className="w-1/20 bg-accent text-accent-foreground" style={{ whiteSpace: 'pre-line' }} key={head.id}>{head.title}</TableHead> : null))}
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableRow className="bg-secondary">
|
||||
{upHeadList.map((head: TableUpperProps) => {
|
||||
if (head.upName === 'upChangeRe1' || head.upName === 'upChangeRe2' ||head.upName === 'upChangeRe3' || head.upName === 'upChangeRe4' || head.upName === 'upChangeRe5' || head.upName === 'upChangeRe6') {
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-gray-50">{head.title}</TableHead>;
|
||||
return <TableHead key={head.id} style={{ whiteSpace: 'pre-line' }} className="w-40 bg-secondary">{head.title}</TableHead>;
|
||||
}
|
||||
})}
|
||||
</TableRow>
|
||||
@@ -84,4 +84,4 @@ const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProp
|
||||
);
|
||||
}
|
||||
|
||||
export { HeadMakeColSpan };
|
||||
export { HeadMakeColSpan };
|
||||
|
||||
@@ -116,7 +116,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[40px]"
|
||||
className="mr-10 w-[200px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="시도"
|
||||
selectArray={sidoData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
|
||||
@@ -125,7 +125,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[65px]"
|
||||
className="mr-10 w-[200px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="시군구"
|
||||
selectArray={filteredSigunData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
|
||||
@@ -134,7 +134,7 @@ export function SearchBar({ onFeatureSelect, gisMaps }: SearchBarProps) {
|
||||
<SelectBox
|
||||
labelClass="w-[140px]"
|
||||
className="mr-3 w-[250px]"
|
||||
selectClass="rounded-lg bg-white"
|
||||
selectClass="rounded-lg"
|
||||
label="하수처리시설"
|
||||
selectArray={facilityData}
|
||||
onSelectValue={(val) => onSelectValue(val.toString(), "fcltCd")}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# apps/fo 테마 적용 작업계획
|
||||
|
||||
작성일: 2026-06-25
|
||||
대상 앱: `D:\iipbs\frontend\apps\fo`
|
||||
|
||||
## 1. 목적
|
||||
|
||||
- `apps/fo` 프로그램 페이지 내부에 직접 박혀 있는 색상 하드코딩을 식별한다.
|
||||
- 가능한 범위는 `packages/common/assets/src/styles/base.css`의 CSS 변수 영향권으로 옮긴다.
|
||||
- 즉 `bg-white`, `text-gray-*`, `border-gray-*`, `bg-blue-*`, `bg-green-*`, `bg-red-*` 같은 직접 색상 표현을 `background`, `foreground`, `label`, `border`, `secondary`, `accent`, `primary`, `destructive` 계열 토큰으로 치환한다.
|
||||
|
||||
## 2. 전제
|
||||
|
||||
- `apps/fo`는 [tailwind.config.ts](D:/iipbs/frontend/apps/fo/tailwind.config.ts:1)에서 공통 preset을 사용한다.
|
||||
- 공통 토큰은 [packages/common/assets/src/styles/base.css](D:/iipbs/frontend/packages/common/assets/src/styles/base.css:1)에 정의되어 있다.
|
||||
- `packages/common/components`, `packages/common/assets`는 기준 레이어이며, 앱 작업은 이 기준을 따르는 방향으로 진행한다.
|
||||
- 단, 이번 요청의 1차 목표는 `apps/fo` 내부 코드에 박혀 있는 색상을 줄이는 것이다.
|
||||
|
||||
## 3. 확인된 하드코딩 유형
|
||||
|
||||
현재 `apps/fo`에서 확인된 대표 패턴은 아래와 같다.
|
||||
|
||||
- 검색 입력/선택 UI
|
||||
- `apps/fo/app/components/common/SearchInput.tsx`
|
||||
- `apps/fo/app/pages/map/searchBar/searchBar.tsx`
|
||||
- `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
|
||||
- `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
|
||||
- 대시보드/안내문구
|
||||
- `apps/fo/app/components/common/UnderComment.tsx`
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
|
||||
- 대시보드 표 셀 상태색
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
|
||||
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
|
||||
- 지도 오버레이/팝업
|
||||
- `apps/fo/app/pages/(map-layout)/map/page.tsx`
|
||||
- 테스트/샘플성 버튼
|
||||
- `apps/fo/app/pages/(with-layout)/test/page.tsx`
|
||||
- 모달 입력창
|
||||
- `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
|
||||
|
||||
## 4. 기본 원칙
|
||||
|
||||
### 4.1 바로 토큰 치환 가능한 경우
|
||||
|
||||
아래는 우선 토큰으로 바로 바꾼다.
|
||||
|
||||
- `bg-white` -> `bg-background` 또는 `bg-card` 또는 `bg-popover`
|
||||
- `text-gray-900`, `text-gray-800` -> `text-foreground`
|
||||
- `text-gray-700`, `text-gray-600` -> `text-label` 또는 `text-foreground`
|
||||
- `text-gray-500` -> `text-muted-foreground` 또는 `text-placeholder`
|
||||
- `border-gray-200`, `border-gray-300` -> `border-border`
|
||||
- `focus:ring-blue-500` -> `focus:ring-ring`
|
||||
- `text-blue-500`, `text-blue-600` -> `text-primary`
|
||||
- `bg-blue-500`, `bg-blue-600` -> `bg-primary`
|
||||
- `text-red-500`, `text-red-600` -> `text-destructive`
|
||||
- `bg-red-50` 등 경고 계열 -> `bg-accent` 또는 별도 위험 상태 규칙 검토
|
||||
|
||||
### 4.2 바로 토큰 치환하면 안 되는 경우
|
||||
|
||||
아래는 의미색일 가능성이 높아 바로 토큰화하지 않는다.
|
||||
|
||||
- 범례 색상
|
||||
- 데이터 구분용 heatmap/구간 색상
|
||||
- 수질/계절/유형별 비교 색상
|
||||
- 지도 오버레이에서 정보 카테고리 의미를 나타내는 색
|
||||
|
||||
이 경우는 다음 중 하나로 처리한다.
|
||||
|
||||
- 유지
|
||||
- 별도 의미 클래스 부여
|
||||
- 앱 전용 CSS로 상태 클래스를 만든 뒤 theme-safe surface만 토큰화
|
||||
|
||||
## 5. 우선순위
|
||||
|
||||
### 5.1 1순위: 일반 UI surface
|
||||
|
||||
가장 먼저 처리한다.
|
||||
|
||||
- 검색 입력창 배경/보더/텍스트
|
||||
- 검색 결과 dropdown 배경/hover
|
||||
- 팝업/모달 wrapper 배경/보더
|
||||
- 제목/라벨/안내문구 색
|
||||
- 버튼의 직접 blue/red/white 색상
|
||||
|
||||
이 영역은 theme 적용 체감이 가장 크고, 토큰 치환도 안전하다.
|
||||
|
||||
### 5.2 2순위: 테이블/리스트
|
||||
|
||||
- `selectedFcltList`
|
||||
- `facilityInfo`
|
||||
- modal 내부 표
|
||||
- overlay 내 표 구조
|
||||
|
||||
이 영역은 `bg-secondary`, `bg-accent`, `text-secondary-foreground`, `border-border` 계열로 정리한다.
|
||||
|
||||
### 5.3 3순위: 의미색 데이터 표현
|
||||
|
||||
- `facilityInfo`의 계절/항목별 `bg-green-*`, `bg-blue-*`
|
||||
- 지도 범례 색상
|
||||
- 지도 feature overlay 강조색
|
||||
- 대시보드 표 셀 상태색
|
||||
- `apps/fo/app/pages/(with-layout)/dashboard/facilityInfo/page.tsx`
|
||||
이 페이지는 특별한 페이지로, 연간, 하절기, 동절기 색을 구분하여 보여주어야 한다. 따라서 text-foreground 가 보이도록 하되 연간, 하절기, 동절기 색을 apps\fo\src\app\styles 아래 테마별 css에 정의해서 보여주어야 한다.
|
||||
|
||||
이 영역은 “없애는 것”보다 “역할을 분리하는 것”이 중요하다.
|
||||
|
||||
## 6. 실제 작업 순서
|
||||
|
||||
1. `apps/fo` 내부 하드코딩 색상 파일 목록 확정
|
||||
2. 공통 UI와 데이터 의미색을 분리
|
||||
3. 일반 UI 색상부터 `base.css` 토큰으로 치환
|
||||
4. 필요 시 `apps/fo/app/styles/*.css`에 앱 전용 의미 클래스 추가
|
||||
5. `dashboard`, `map`, `modal`, `search` 대표 화면 점검
|
||||
6. 남은 의미색 영역은 별도 상태색 정책으로 정리
|
||||
|
||||
## 7. 권장 치환 전략
|
||||
|
||||
### 7.1 JSX 안에서 바로 치환
|
||||
|
||||
아래처럼 className을 직접 토큰 기반으로 변경한다.
|
||||
|
||||
- `bg-white` -> `bg-background`
|
||||
- `text-gray-600` -> `text-label`
|
||||
- `border-gray-300` -> `border-border`
|
||||
|
||||
이 방식은 빠르지만 반복이 많으면 유지보수성이 떨어진다.
|
||||
|
||||
### 7.2 반복되는 UI는 앱 전용 클래스 도입
|
||||
|
||||
예:
|
||||
|
||||
- `fo-search-panel`
|
||||
- `fo-search-input`
|
||||
- `fo-popup-card`
|
||||
- `fo-empty-text`
|
||||
|
||||
반복되는 구조는 `apps/fo/app/styles/components*.css`에 올리고, JSX에서는 의미 클래스만 사용한다.
|
||||
|
||||
이 방식이 장기적으로 더 좋다.
|
||||
|
||||
## 8. 예상 주의사항
|
||||
|
||||
- `h-90` 같은 Tailwind v3 미지원 클래스가 섞여 있을 수 있다.
|
||||
- `bg-white`를 `bg-background`로 바꾸면 black theme에서 contrast가 크게 달라질 수 있으므로 텍스트 색도 같이 바꿔야 한다.
|
||||
- `bg-blue-*`, `bg-green-*`는 실제 status color인지 단순 강조색인지 구분하고 바꿔야 한다.
|
||||
- 지도 오버레이는 HTML string으로 만드는 부분이 있으면 className 치환만으로 끝나지 않을 수 있다.
|
||||
|
||||
## 9. 1차 적용 추천 대상
|
||||
|
||||
우선 아래 파일부터 적용하는 것이 효율적이다.
|
||||
|
||||
1. `apps/fo/app/components/common/SearchInput.tsx`
|
||||
2. `apps/fo/app/pages/map/searchBar/searchBar.tsx`
|
||||
3. `apps/fo/app/components/common/UnderComment.tsx`
|
||||
4. `apps/fo/app/pages/(with-layout)/dashboard/rainManage/page.tsx`
|
||||
5. `apps/fo/app/components/common/FacilityTermModalHeader.tsx`
|
||||
6. `apps/fo/app/components/common/FacilityTermModalHeaderMonth.tsx`
|
||||
7. `apps/fo/app/pages/(with-layout)/facilityPrecipitation/modal/page.tsx`
|
||||
|
||||
## 10. 결론
|
||||
|
||||
- `apps/fo`도 `apps/wr`와 동일하게 `base.css` 토큰 중심으로 정리 가능하다.
|
||||
- 1차는 일반 UI 하드코딩 제거에 집중하고, 데이터 의미색은 유지 또는 별도 정책으로 분리하는 것이 맞다.
|
||||
- 가장 안전한 방향은 “반복 UI는 앱 전용 의미 클래스”, “일회성은 JSX에서 직접 토큰 치환”의 혼합 방식이다.
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "./styles/components-black.css";
|
||||
import "./styles/components-blue.css";
|
||||
import "./styles/components-green.css";
|
||||
import "./styles/components-red.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 20%, var(--secondary));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 20%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 20%, var(--secondary));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--secondary));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--secondary));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 36%, var(--secondary));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 36%, var(--secondary));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 36%, var(--secondary));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 16%, var(--secondary));
|
||||
--fo-facility-info-header-bg: #5e7b9c;
|
||||
--fo-facility-info-header-fg: #ffffff;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 16%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 16%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 16%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 24%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 24%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 24%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 32%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 32%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 32%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #eaf3fb;
|
||||
--fo-facility-info-header-fg: #647784;
|
||||
--fo-facility-info-header-border: #e5e5e5;
|
||||
|
||||
.card-border {
|
||||
@apply border-[#d8e3eb] bg-white shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#d8e3eb];
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply text-[#4b5563];
|
||||
}
|
||||
|
||||
.card.card-border {
|
||||
box-shadow: 0 1px 8px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.card.card-border .card.card-border {
|
||||
@apply border-[#dde6ed] bg-white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card.card-border input,
|
||||
.card.card-border button[role="combobox"] {
|
||||
@apply border-[#d8e3eb] bg-white text-[#737373];
|
||||
}
|
||||
|
||||
.card.card-border label {
|
||||
@apply text-[#666666];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
nav[aria-label="pagination"] li a {
|
||||
border-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a:hover {
|
||||
background-color: #f6f6f6 !important;
|
||||
color: #0a0a0a !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a[aria-current="page"] {
|
||||
border-color: #2094fa !important;
|
||||
background-color: #2094fa !important;
|
||||
color: #fafafa !important;
|
||||
}
|
||||
|
||||
nav[aria-label="pagination"] li a[aria-label="Go to previous page"],
|
||||
nav[aria-label="pagination"] li a[aria-label="Go to next page"] {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
color: #666666 !important;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 15%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 15%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 15%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 23%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 23%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 23%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 31%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 31%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 31%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-3) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #ebf7f7;
|
||||
--fo-facility-info-header-fg: #64908f;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 14%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 14%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 14%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 22%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 22%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 22%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 30%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 30%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 30%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--destructive) 12%, var(--background));
|
||||
--fo-facility-info-header-bg: #f6f6f9;
|
||||
--fo-facility-info-header-fg: #808080;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
:root {
|
||||
--fo-facility-info-flow-year-bg: color-mix(in srgb, var(--chart-4) 18%, var(--background));
|
||||
--fo-facility-info-water-year-bg: color-mix(in srgb, var(--chart-3) 18%, var(--background));
|
||||
--fo-facility-info-phosphorus-year-bg: color-mix(in srgb, var(--chart-1) 18%, var(--background));
|
||||
--fo-facility-info-flow-summer-bg: color-mix(in srgb, var(--chart-4) 28%, var(--background));
|
||||
--fo-facility-info-water-summer-bg: color-mix(in srgb, var(--chart-3) 28%, var(--background));
|
||||
--fo-facility-info-phosphorus-summer-bg: color-mix(in srgb, var(--chart-1) 28%, var(--background));
|
||||
--fo-facility-info-flow-winter-bg: color-mix(in srgb, var(--chart-4) 38%, var(--background));
|
||||
--fo-facility-info-water-winter-bg: color-mix(in srgb, var(--chart-3) 38%, var(--background));
|
||||
--fo-facility-info-phosphorus-winter-bg: color-mix(in srgb, var(--chart-1) 38%, var(--background));
|
||||
--fo-rain-impact-row-bg: color-mix(in srgb, var(--chart-1) 10%, var(--background));
|
||||
--fo-facility-info-header-bg: #eaf3fb;
|
||||
--fo-facility-info-header-fg: #647784;
|
||||
--fo-facility-info-header-border: var(--border);
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
@@ -14,13 +30,63 @@
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
.fo-facility-info-flow-year,
|
||||
.fo-facility-info-water-year,
|
||||
.fo-facility-info-phosphorus-year,
|
||||
.fo-facility-info-flow-summer,
|
||||
.fo-facility-info-water-summer,
|
||||
.fo-facility-info-phosphorus-summer,
|
||||
.fo-facility-info-flow-winter,
|
||||
.fo-facility-info-water-winter,
|
||||
.fo-facility-info-phosphorus-winter {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-year {
|
||||
background-color: var(--fo-facility-info-flow-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-year {
|
||||
background-color: var(--fo-facility-info-water-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-year {
|
||||
background-color: var(--fo-facility-info-phosphorus-year-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-summer {
|
||||
background-color: var(--fo-facility-info-flow-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-summer {
|
||||
background-color: var(--fo-facility-info-water-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-summer {
|
||||
background-color: var(--fo-facility-info-phosphorus-summer-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-flow-winter {
|
||||
background-color: var(--fo-facility-info-flow-winter-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-water-winter {
|
||||
background-color: var(--fo-facility-info-water-winter-bg);
|
||||
}
|
||||
|
||||
.fo-facility-info-phosphorus-winter {
|
||||
background-color: var(--fo-facility-info-phosphorus-winter-bg);
|
||||
}
|
||||
|
||||
.fo-rain-impact-row td {
|
||||
background-color: var(--fo-rain-impact-row-bg);
|
||||
}
|
||||
|
||||
|
||||
.fo-facility-info-header-surface {
|
||||
border: 1px solid var(--fo-facility-info-header-border);
|
||||
background-color: var(--fo-facility-info-header-bg);
|
||||
color: var(--fo-facility-info-header-fg);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
|
||||
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "./styles/components-black.css";
|
||||
import "./styles/components-blue.css";
|
||||
import "./styles/components-green.css";
|
||||
import "./styles/components-red.css";
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666];
|
||||
}
|
||||
.card-border {
|
||||
@apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur;
|
||||
}
|
||||
.card-header {
|
||||
@apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4;
|
||||
}
|
||||
.card-footer {
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,6 @@
|
||||
@apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
@apply border-[#3f4042] bg-[rgba(255,255,255,0.065)];
|
||||
}
|
||||
.card-header {
|
||||
@apply border-b-[#3f4042];
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@apply w-[200px] h-[40px] px-3 py-2 border border-[#d1d5db] rounded-md bg-white text-sm cursor-pointer;
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ NEXT_PUBLIC_PROXY_HOST=http://localhost:3001
|
||||
SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_SITE_ID=ST
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
NEXT_PUBLIC_ST_DASHBOARD_BBS_ID=13
|
||||
@@ -1,54 +1,32 @@
|
||||
"use client";
|
||||
|
||||
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
|
||||
getTabStoreState as getGridTabStoreState,
|
||||
|
||||
useTabStore as gridTabStore,
|
||||
|
||||
} from "@rsone/components";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
|
||||
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
|
||||
import {
|
||||
menuMatchesStatisticsTabId,
|
||||
resolveStatisticsTabId,
|
||||
} from "@/shared/lib/statistics-menu-url";
|
||||
import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync";
|
||||
|
||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
|
||||
import { tokenManager } from "@/entities/auth";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
|
||||
|
||||
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
|
||||
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
const appConfig = getConfig();
|
||||
|
||||
@@ -58,42 +36,23 @@ const appSiteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
|
||||
|
||||
function WithBaseLayoutInner({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
|
||||
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
|
||||
|
||||
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
|
||||
|
||||
useState(false);
|
||||
|
||||
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
|
||||
);
|
||||
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
@@ -108,16 +67,11 @@ function WithBaseLayoutInner({
|
||||
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
|
||||
|
||||
const { syncTabToUrl } = useStatisticsTabUrlSync({
|
||||
|
||||
pathname,
|
||||
|
||||
menuList,
|
||||
@@ -129,41 +83,27 @@ function WithBaseLayoutInner({
|
||||
addTabForGrid,
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
|
||||
queryFn: () => MenuApi.getMenuRoles(appSiteId),
|
||||
|
||||
enabled: isLoggedIn,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (menuListData) {
|
||||
|
||||
setMenuList(menuListData);
|
||||
|
||||
setMenuListForGrid(menuListData);
|
||||
|
||||
}
|
||||
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
|
||||
|
||||
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!menuList) return;
|
||||
|
||||
const appTabs = getTabStoreState().tabs;
|
||||
@@ -172,20 +112,13 @@ function WithBaseLayoutInner({
|
||||
|
||||
if (getGridTabStoreState().tabs.length > 0) return;
|
||||
|
||||
|
||||
|
||||
for (const tab of appTabs) {
|
||||
|
||||
getGridTabStoreState().addTab(tab.menuItem);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const activeId = getTabStoreState().activeTabId;
|
||||
|
||||
if (activeId) {
|
||||
|
||||
setActiveTab(activeId);
|
||||
|
||||
const activeTab = appTabs.find((t) => t.id === activeId);
|
||||
@@ -193,155 +126,104 @@ function WithBaseLayoutInner({
|
||||
const gridActiveId = activeTab?.menuItem.menuEngNm;
|
||||
|
||||
if (gridActiveId) {
|
||||
|
||||
getGridTabStoreState().setActiveTab(gridActiveId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, [menuList, tabs, setActiveTab]);
|
||||
|
||||
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
|
||||
const currentPath = useMemo(() => {
|
||||
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
|
||||
for (const item of items) {
|
||||
|
||||
if (item.urlPath === activeTabId) {
|
||||
|
||||
if (menuMatchesStatisticsTabId(item, activeTabId)) {
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
|
||||
const found = findMenuByEngNm(item.children);
|
||||
|
||||
if (found) return found;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
|
||||
return activeMenu.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
|
||||
if (activeTabId) {
|
||||
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
|
||||
return activeTab.menuItem.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
|
||||
return currentMenuInfo.urlPath;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
|
||||
return pathname ?? "";
|
||||
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
|
||||
(item: MenuItem) => {
|
||||
if (item.urlPath.includes("/cms/")) return;
|
||||
|
||||
setCurrentMenuInfo(item);
|
||||
|
||||
|
||||
|
||||
if (isVerificationSelectionReportMenu(item)) {
|
||||
|
||||
setReportPanelOpen(true);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (isBbsNoticeMenu(item)) {
|
||||
if (item.urlPath) {
|
||||
router.push(item.urlPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
|
||||
if (addTabResult) {
|
||||
|
||||
addTabForGrid(item);
|
||||
|
||||
if (item.urlPath) {
|
||||
|
||||
syncTabToUrl(item.urlPath);
|
||||
|
||||
syncTabToUrl(resolveStatisticsTabId(item.urlPath));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!addTabResult) {
|
||||
|
||||
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
|
||||
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
[
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
addTab,
|
||||
@@ -354,70 +236,38 @@ function WithBaseLayoutInner({
|
||||
|
||||
syncTabToUrl,
|
||||
|
||||
router,
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<BaseLayout
|
||||
|
||||
queryClient={queryClient}
|
||||
|
||||
tokenManager={tokenManager}
|
||||
|
||||
currentPath={currentPath}
|
||||
|
||||
handleMenuClick={handleMenuClick}
|
||||
|
||||
>
|
||||
|
||||
<div className="flex flex-1 h-full">
|
||||
|
||||
<div className="flex h-full flex-1">
|
||||
{isVerificationSelectionReportPanelOpen && (
|
||||
|
||||
<VerificationSelectionReport
|
||||
|
||||
onClose={() => setReportPanelOpen(false)}
|
||||
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
|
||||
</div>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function WithBaseLayout({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
return (
|
||||
|
||||
<Suspense fallback={null}>
|
||||
|
||||
<WithBaseLayoutInner>{children}</WithBaseLayoutInner>
|
||||
|
||||
</Suspense>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
/* 셀 클래스가 있으면 위 기본색보다 우선 (같은 specificity + 나중 규칙) */
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
|
||||
@apply !bg-[#f9f8f8] !text-[#666];
|
||||
@apply !bg-[#f0f0f0] !text-[#666];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error {
|
||||
@apply !bg-[#ffe6e6] !text-[#c00];
|
||||
|
||||
@@ -43,8 +43,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
||||
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
|
||||
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
|
||||
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) {
|
||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
|
||||
import {
|
||||
getExcelImportColumnFieldNames,
|
||||
prepareGridColumnsForExcelExport,
|
||||
restoreGridColumnsAfterExcelExport,
|
||||
shouldPrepareColumnsForExcelExport,
|
||||
@@ -33,7 +34,7 @@ declare module "realgrid" {
|
||||
setOptions(options: {
|
||||
footer?: { visible?: boolean };
|
||||
summaryMode?: "aggregate" | "statistical" | "none";
|
||||
checkBar?: { visible?: boolean };
|
||||
checkBar?: { visible?: boolean; exclusive?: boolean };
|
||||
}): void;
|
||||
}
|
||||
}
|
||||
@@ -457,10 +458,31 @@ function sortRowsGroupPreserving(
|
||||
}
|
||||
|
||||
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
|
||||
function getDecimalPlacesFromDataFormat(dataFormat?: string): number | null {
|
||||
if (!dataFormat) return null;
|
||||
const match = dataFormat.match(/\.(0+|#+)/);
|
||||
const decimalToken = match?.[1];
|
||||
if (decimalToken) return decimalToken.length;
|
||||
// #,##0 등 정수 포맷 — 소수부 없음 → 0자리 절삭(내림)
|
||||
if (/[#0]/.test(dataFormat)) return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
function truncateByDecimalPlaces(value: number, decimalPlaces: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
if (decimalPlaces < 0) return value;
|
||||
const factor = 10 ** decimalPlaces;
|
||||
const scaled = value * factor;
|
||||
// 절삭(truncate): 반올림 없이 소수점 이하를 버린다.
|
||||
const truncated = scaled >= 0 ? Math.floor(scaled) : Math.ceil(scaled);
|
||||
return truncated / factor;
|
||||
}
|
||||
|
||||
function buildFieldsFromColumns(
|
||||
columns: {
|
||||
fieldName: string;
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
datetimeFormat?: string;
|
||||
editor?: { type?: string; datetimeFormat?: string };
|
||||
computed?: import("../type/DataGrid.type").ComputedFieldConfig;
|
||||
@@ -508,18 +530,52 @@ function buildFieldsFromColumns(
|
||||
"minuendFields" in col.computed &&
|
||||
"subtrahendFields" in col.computed &&
|
||||
col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums =
|
||||
col.computed.type === "halfSumOfSums" &&
|
||||
"firstSumFields" in col.computed &&
|
||||
"secondSumFields" in col.computed &&
|
||||
col.computed.firstSumFields.length > 0 &&
|
||||
col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums =
|
||||
col.computed.type === "sumOfTwoHalfSumOfSums" &&
|
||||
"firstHalfSum" in col.computed &&
|
||||
"secondHalfSum" in col.computed &&
|
||||
col.computed.firstHalfSum.firstSumFields.length > 0 &&
|
||||
col.computed.firstHalfSum.secondSumFields.length > 0 &&
|
||||
col.computed.secondHalfSum.firstSumFields.length > 0 &&
|
||||
col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups =
|
||||
col.computed.type === "sumOfMaxOfFieldAndSumGroups" &&
|
||||
"groups" in col.computed &&
|
||||
Array.isArray(col.computed.groups) &&
|
||||
col.computed.groups.length > 0;
|
||||
const hasPumpGrossPpqty =
|
||||
col.computed.type === "pumpGrossPpqty" &&
|
||||
"pumpGroups" in col.computed &&
|
||||
col.computed.pumpGroups.length > 0;
|
||||
const hasBodPrcsRt =
|
||||
col.computed.type === "bodPrcsRt" &&
|
||||
"inflwField" in col.computed &&
|
||||
"dschgField" in col.computed;
|
||||
const hasQuotientRatioPercent =
|
||||
col.computed.type === "quotientRatioPercent" &&
|
||||
"numeratorQuotient" in col.computed &&
|
||||
"denominatorQuotient" in col.computed &&
|
||||
col.computed.numeratorQuotient.length === 2 &&
|
||||
col.computed.denominatorQuotient.length === 2;
|
||||
if (
|
||||
hasFields ||
|
||||
hasNumeratorDenominator ||
|
||||
hasMaxOfFieldAndSum ||
|
||||
hasDifferenceOfSums ||
|
||||
hasSumOfMaxGroups
|
||||
hasHalfSumOfSums ||
|
||||
hasSumOfTwoHalfSumOfSums ||
|
||||
hasSumOfMaxGroups ||
|
||||
hasPumpGrossPpqty ||
|
||||
hasBodPrcsRt ||
|
||||
hasQuotientRatioPercent
|
||||
) {
|
||||
field.valueCallback = getComputedValueCallback(
|
||||
const computedCallback = getComputedValueCallback(
|
||||
col.computed,
|
||||
) as (
|
||||
prod: unknown,
|
||||
@@ -528,6 +584,24 @@ function buildFieldsFromColumns(
|
||||
fieldNames: string[],
|
||||
values: unknown[],
|
||||
) => number;
|
||||
const decimalPlaces = getDecimalPlacesFromDataFormat(col.dataFormat);
|
||||
field.valueCallback = (
|
||||
prod: unknown,
|
||||
dataRow: number,
|
||||
fieldName: string,
|
||||
fieldNames: string[],
|
||||
values: unknown[],
|
||||
) => {
|
||||
const computedValue = computedCallback(
|
||||
prod,
|
||||
dataRow,
|
||||
fieldName,
|
||||
fieldNames,
|
||||
values,
|
||||
);
|
||||
if (decimalPlaces == null) return computedValue;
|
||||
return truncateByDecimalPlaces(computedValue, decimalPlaces);
|
||||
};
|
||||
}
|
||||
}
|
||||
return field;
|
||||
@@ -586,6 +660,7 @@ const DataGrid = ({
|
||||
const onItemAllCheckedRef =
|
||||
useRef<typeof onItemAllChecked>(onItemAllChecked);
|
||||
const isSyncingChecksFromRowDataRef = useRef(false);
|
||||
const columnsFieldsSigRef = useRef("");
|
||||
columnsRef.current = columns;
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
@@ -638,20 +713,6 @@ const DataGrid = ({
|
||||
.find((s) => s.menuEngNm === activeTabId)?.menuKornNm ||
|
||||
"export";
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || "";
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (
|
||||
layout: ColumnLayoutItem[],
|
||||
@@ -745,20 +806,6 @@ const DataGrid = ({
|
||||
excelExportVisibilitySnapshot,
|
||||
);
|
||||
}
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (
|
||||
gridCol &&
|
||||
gridCol.header &&
|
||||
originalHeaders.has(col.name)
|
||||
) {
|
||||
gridCol.header.text =
|
||||
originalHeaders.get(col.name) || "";
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
@@ -860,36 +907,11 @@ const DataGrid = ({
|
||||
: headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(
|
||||
/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/,
|
||||
);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (
|
||||
columns.find(
|
||||
(c) => c.fieldName === extractedFieldName,
|
||||
)
|
||||
) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(
|
||||
`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`,
|
||||
);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
// 엑셀 N번째 열 → 그리드 N번째 fieldName (보내기 컬럼 순서 기준)
|
||||
const excelColumnFieldNames = getExcelImportColumnFieldNames(
|
||||
columns,
|
||||
columnLayout,
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -907,12 +929,8 @@ const DataGrid = ({
|
||||
worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: col })
|
||||
];
|
||||
const headerName =
|
||||
headers[colIndex] || `Column${colIndex + 1}`;
|
||||
// 헤더 텍스트를 fieldName으로 변환
|
||||
const fieldName =
|
||||
headerToFieldNameMap.get(headerName) ||
|
||||
headerName;
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
if (!fieldName) continue;
|
||||
|
||||
let cellValue: string | number = "";
|
||||
if (
|
||||
@@ -996,6 +1014,21 @@ const DataGrid = ({
|
||||
const dataProvider = gridRef.current.dataProvider;
|
||||
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
|
||||
|
||||
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
|
||||
if (columnsFieldsSig !== columnsFieldsSigRef.current) {
|
||||
columnsFieldsSigRef.current = columnsFieldsSig;
|
||||
const dpForFields = dataProvider as {
|
||||
setFields?: (f: unknown[]) => void;
|
||||
setRows?: (r: Record<string, string | number>[]) => void;
|
||||
};
|
||||
try {
|
||||
dpForFields.setFields?.(buildFieldsFromColumns(columns));
|
||||
dpForFields.setRows?.(rowDataRef.current);
|
||||
} catch (e) {
|
||||
console.warn("setFields sync:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
|
||||
const hasMergeCells = columns.some((col) => col.mergeCells === true);
|
||||
|
||||
@@ -1046,6 +1079,14 @@ const DataGrid = ({
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
const gv = gridView as GridViewWithSetColumnProperty;
|
||||
const exists =
|
||||
typeof (
|
||||
gridView as { columnByName?: (name: string) => unknown }
|
||||
).columnByName === "function" &&
|
||||
(gridView as { columnByName: (name: string) => unknown }).columnByName(
|
||||
column.name,
|
||||
) != null;
|
||||
if (!exists) return;
|
||||
gv.setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
@@ -1054,6 +1095,11 @@ const DataGrid = ({
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
gv.setColumnProperty?.(
|
||||
column.name,
|
||||
"editable",
|
||||
column.editable !== false,
|
||||
);
|
||||
applyColumnListEditorProperties(gv, column);
|
||||
});
|
||||
if (columns.some((c) => isDropDownEditorColumn(c))) {
|
||||
@@ -1101,7 +1147,7 @@ const DataGrid = ({
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
checkBar: { visible: showHeaderCheckbox },
|
||||
checkBar: { visible: showHeaderCheckbox, exclusive: false },
|
||||
rowIndicator: { visible: false },
|
||||
});
|
||||
|
||||
@@ -1181,10 +1227,63 @@ const DataGrid = ({
|
||||
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
|
||||
|
||||
if (hasFooterUserSpans) {
|
||||
const validColumnNames = new Set(columns.map((c) => c.name));
|
||||
const sanitizeColumnLayout = (
|
||||
layout: unknown[],
|
||||
): unknown[] => {
|
||||
const result: unknown[] = [];
|
||||
for (const item of layout) {
|
||||
if (typeof item === "string") {
|
||||
if (validColumnNames.has(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!item || typeof item !== "object") continue;
|
||||
|
||||
if ("column" in item) {
|
||||
const columnName = (item as { column?: unknown }).column;
|
||||
if (
|
||||
typeof columnName === "string" &&
|
||||
validColumnNames.has(columnName)
|
||||
) {
|
||||
result.push(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("items" in item && Array.isArray((item as { items?: unknown[] }).items)) {
|
||||
const nextItems = sanitizeColumnLayout(
|
||||
(item as { items: unknown[] }).items,
|
||||
);
|
||||
if (nextItems.length > 0) {
|
||||
result.push({
|
||||
...(item as Record<string, unknown>),
|
||||
items: nextItems,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
|
||||
setTimeout(() => {
|
||||
if (gridRef.current?.gridView && columnLayout) {
|
||||
gridRef.current.gridView.setColumnLayout?.(columnLayout);
|
||||
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
|
||||
if (safeLayout.length === 0) return;
|
||||
try {
|
||||
(
|
||||
gridRef.current.gridView.setColumnLayout as
|
||||
| ((layout: unknown) => void)
|
||||
| undefined
|
||||
)?.(safeLayout);
|
||||
} catch (e) {
|
||||
console.warn("setColumnLayout skipped due to invalid columns:", e);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
@@ -1654,6 +1753,22 @@ const DataGrid = ({
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onRGDataColumnsChanged={(grid) => {
|
||||
const dp = gridRef.current?.dataProvider as
|
||||
| {
|
||||
setFields?: (f: unknown[]) => void;
|
||||
setRows?: (
|
||||
r: Record<string, string | number>[],
|
||||
) => void;
|
||||
}
|
||||
| undefined;
|
||||
try {
|
||||
dp?.setFields?.(
|
||||
buildFieldsFromColumns(columnsRef.current),
|
||||
);
|
||||
dp?.setRows?.(rowDataRef.current);
|
||||
} catch (e) {
|
||||
console.warn("onRGDataColumnsChanged setFields:", e);
|
||||
}
|
||||
reapplyListEditorsOnGrid(
|
||||
grid as GridViewWithSetColumnProperty,
|
||||
columnsRef.current,
|
||||
@@ -1693,6 +1808,9 @@ const DataGrid = ({
|
||||
dataProvider.setFields(
|
||||
buildFieldsFromColumns(columns),
|
||||
);
|
||||
columnsFieldsSigRef.current = columns
|
||||
.map((c) => c.fieldName)
|
||||
.join("\u0001");
|
||||
}
|
||||
if (dataProvider?.setRows)
|
||||
dataProvider.setRows(rowData);
|
||||
@@ -1874,15 +1992,9 @@ const DataGrid = ({
|
||||
{columns.map((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
|
||||
const columnReactKey =
|
||||
column.values?.length &&
|
||||
isDropDownEditorColumn(column)
|
||||
? `${column.name}-dropdown-${column.values.length}`
|
||||
: column.name;
|
||||
|
||||
return (
|
||||
<RGDataColumn
|
||||
key={columnReactKey}
|
||||
key={column.name}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={
|
||||
|
||||
@@ -33,7 +33,7 @@ import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
|
||||
/** 통계 입력 현황 > 행 클릭 시 사용자예외처리(prcsYn=2) 내역만 조회 */
|
||||
export interface ExceptionProcessingHistoryQueryContext {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
@@ -92,13 +92,15 @@ function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams)
|
||||
const exceptionHistoryColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
|
||||
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
|
||||
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{
|
||||
@@ -114,13 +116,15 @@ const exceptionHistoryColumnInfo: ColumnInfo = {
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"pbcpAprvYn",
|
||||
"stVrfRjtComNm",
|
||||
"prcsOpnnExpln",
|
||||
"rjctOpnnCn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"individualReject",
|
||||
@@ -190,9 +194,9 @@ export function ExceptionProcessingHistoryDialog({
|
||||
sggNm: queryContext.sggNm,
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: PRCS_YN_LIST,
|
||||
rejectCodeList: "0,1,2",
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
@@ -308,14 +312,16 @@ export function ExceptionProcessingHistoryDialog({
|
||||
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
|
||||
rjctOpnnCn: item.rjctOpnnCn ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
individualReject: "개별 반려",
|
||||
|
||||
@@ -41,7 +41,7 @@ const getSggListFetcher = async (
|
||||
key: [string, string, string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
||||
return statisticsApi.getSggAllList(key[1], {
|
||||
return statisticsApi.getSggList(key[1], {
|
||||
instSeCd: key[2],
|
||||
gugunSkipYn,
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
const getSggAllListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsApi.getSggAllList(key[1]);
|
||||
return statisticsApi.getSggList(key[1]);
|
||||
};
|
||||
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
@@ -292,17 +292,12 @@ const SidoSggFacilityNameSearchForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const ctpvNm = String(row.ctpvNm ?? sido);
|
||||
const sggNm = String(
|
||||
row.sggNm ?? (gugun !== "ALL" && gugun ? gugun : ""),
|
||||
);
|
||||
|
||||
const params: SeweragePipeFacilityAdminAreaListRequest = {
|
||||
statsYr: statsYr.trim(),
|
||||
swtrmtFcltCd,
|
||||
sewerBzmnCd: initialSearchValues?.bsnmCd || '',
|
||||
ctpvNm,
|
||||
...(doNameList.includes(ctpvNm) ? { sggNm } : {}),
|
||||
ctpvNm: initialSidoValue || '',
|
||||
...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -17,6 +17,7 @@ import SplitDataGrid from "./SplitDataGrid";
|
||||
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||
import {
|
||||
getExcelImportColumnFieldNames,
|
||||
prepareGridColumnsForExcelExport,
|
||||
restoreGridColumnsAfterExcelExport,
|
||||
shouldPrepareColumnsForExcelExport,
|
||||
@@ -222,8 +223,10 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
|
||||
const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0;
|
||||
const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0;
|
||||
const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0;
|
||||
const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0;
|
||||
const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0;
|
||||
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasSumOfMaxGroups) {
|
||||
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) {
|
||||
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number;
|
||||
}
|
||||
}
|
||||
@@ -373,20 +376,6 @@ const TabDataGrid = ({
|
||||
const columns = columnInfoItem.columns || [];
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || '';
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => {
|
||||
return layout.map(item => {
|
||||
@@ -452,15 +441,6 @@ const TabDataGrid = ({
|
||||
if (excelExportVisibilitySnapshot) {
|
||||
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
|
||||
}
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header && originalHeaders.has(col.name)) {
|
||||
gridCol.header.text = originalHeaders.get(col.name) || '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
@@ -546,29 +526,12 @@ const TabDataGrid = ({
|
||||
const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
const columns = columnInfoItem.columns || [];
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (columns.find(c => c.fieldName === extractedFieldName)) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
const excelColumnFieldNames = getExcelImportColumnFieldNames(
|
||||
columns,
|
||||
columnLayout,
|
||||
);
|
||||
|
||||
// 데이터 행 읽기
|
||||
const dataRows: Record<string, string | number>[] = [];
|
||||
@@ -579,9 +542,8 @@ const TabDataGrid = ({
|
||||
for (let colIndex = 0; colIndex < headers.length; colIndex++) {
|
||||
const col = range.s.c + colIndex;
|
||||
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: col })];
|
||||
const headerName = headers[colIndex] || `Column${colIndex + 1}`;
|
||||
// 헤더 텍스트를 fieldName으로 변환
|
||||
const fieldName = headerToFieldNameMap.get(headerName) || headerName;
|
||||
const fieldName = excelColumnFieldNames[colIndex];
|
||||
if (!fieldName) continue;
|
||||
|
||||
let cellValue: string | number = '';
|
||||
if (cell && cell.v !== undefined && cell.v !== null) {
|
||||
|
||||
@@ -92,13 +92,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
||||
{ name: "vrfcMthdNm", fieldName: "vrfcMthdNm", header: { text: "구분" }, editable: false },
|
||||
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{
|
||||
@@ -111,13 +110,12 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
|
||||
columnLayout: [
|
||||
"vrfcMthdNm",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYn",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsOpnnExpln",
|
||||
@@ -202,13 +200,12 @@ export function VerificationResultDialog({
|
||||
vrfcMthdCd,
|
||||
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
prcsYn: item.prcsYnNm ?? PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "",
|
||||
@@ -226,9 +223,9 @@ export function VerificationResultDialog({
|
||||
sggNm: stableQueryContext.sggNm,
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: page + 1,
|
||||
size: 10000,
|
||||
};
|
||||
|
||||
@@ -137,12 +137,15 @@ function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackPara
|
||||
const verificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "stVrfRjtComNm", fieldName: "stVrfRjtComNm", header: { text: "반려상태" }, editable: false },
|
||||
{ name: "prcsOpnnExpln", fieldName: "prcsOpnnExpln", header: { text: "처리의견" }, editable: false },
|
||||
{ name: "rjctOpnnCn", fieldName: "rjctOpnnCn", header: { text: "반려의견" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
|
||||
@@ -150,12 +153,15 @@ const verificationResultColumnInfo: ColumnInfo = {
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"stVrfRjtComNm",
|
||||
"prcsOpnnExpln",
|
||||
"rjctOpnnCn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsYn",
|
||||
@@ -175,6 +181,8 @@ export interface VerificationResultHistoryDialogProps {
|
||||
} | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
/** 예외 제출 취소 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */
|
||||
onExceptionResetSubmitted?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function VerificationResultHistoryDialog({
|
||||
@@ -183,6 +191,7 @@ export function VerificationResultHistoryDialog({
|
||||
queryContext,
|
||||
title = "검증 결과 내역",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
onExceptionResetSubmitted,
|
||||
}: VerificationResultHistoryDialogProps) {
|
||||
const stableQueryContext = useMemo(() => {
|
||||
if (!queryContext) return null;
|
||||
@@ -245,9 +254,9 @@ export function VerificationResultHistoryDialog({
|
||||
sggNm: stableQueryContext.sggNm,
|
||||
statsYr: stableQueryContext.statsYr,
|
||||
statsCd: stableQueryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0,2,3",
|
||||
prcsYnList: "0,2",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: pageIndex + 1,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
@@ -376,14 +385,16 @@ export function VerificationResultHistoryDialog({
|
||||
vrfcSn: item.vrfcSn,
|
||||
__checked: isChecked ? 1 : 0,
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
admdstNm: item.admdstNm ?? item.sggNm ?? "-",
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
stVrfRjtComNm: item.stVrfRjtComNm ?? "-",
|
||||
prcsOpnnExpln: item.prcsOpnnExpln ?? "-",
|
||||
rjctOpnnCn: item.rjctOpnnCn ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
};
|
||||
@@ -451,12 +462,28 @@ export function VerificationResultHistoryDialog({
|
||||
alert("예외 제출 취소 처리되었습니다.");
|
||||
setCheckedVrfcByTab((prev) => ({ ...prev, [tab]: new Map() }));
|
||||
await loadTabPage(tab, tabPages[tab].pageIndex);
|
||||
try {
|
||||
await onExceptionResetSubmitted?.();
|
||||
} catch {
|
||||
alert(
|
||||
"예외 제출 취소는 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
alert("예외 제출 취소 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setResetSubmitting(false);
|
||||
}
|
||||
}, [activeTab, alert, checkedVrfcByTab, entriesByTab, loadTabPage, stableQueryContext, tabPages]);
|
||||
}, [
|
||||
activeTab,
|
||||
alert,
|
||||
checkedVrfcByTab,
|
||||
entriesByTab,
|
||||
loadTabPage,
|
||||
onExceptionResetSubmitted,
|
||||
stableQueryContext,
|
||||
tabPages,
|
||||
]);
|
||||
|
||||
const renderActiveTabPanel = (tab: VerificationHistoryTabKey) => {
|
||||
const entries = entriesByTab[tab];
|
||||
|
||||
@@ -34,8 +34,8 @@ export async function hasAnyPostSubmitVerificationResult(
|
||||
sggNm: ctx.sggNm,
|
||||
statsYr: ctx.statsYr,
|
||||
statsCd: ctx.statsCd,
|
||||
pbcpAprvYn: "N" as const,
|
||||
prcsYnList: "0",
|
||||
rejectCodeList: "0,1,2",
|
||||
page: 1,
|
||||
size: 1,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
type InfiniteData,
|
||||
keepPreviousData,
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { useTabStore as useGridTabStore } from "@rsone/components";
|
||||
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
|
||||
import type {
|
||||
VerificationResultListRequest,
|
||||
VerificationResultListResponse,
|
||||
VerificationResultReportItem,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
VerificationInputReportItem,
|
||||
VerificationInputReportResponse,
|
||||
VerificationInputReportTypeCountingInfo,
|
||||
} from "@/entities/statistics/model/verification-input-report-interface";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import {
|
||||
findMenuItemByUrlPath,
|
||||
@@ -59,8 +59,8 @@ const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
const PAGE_SIZE = 100;
|
||||
const SCROLL_LOAD_THRESHOLD_PX = 120;
|
||||
|
||||
function getNextVerificationResultPage(
|
||||
lastPage: VerificationResultListResponse,
|
||||
function getNextVerificationInputReportPage(
|
||||
lastPage: VerificationInputReportResponse,
|
||||
): number | undefined {
|
||||
const pageInfo = lastPage?.data?.pageInfo;
|
||||
if (!pageInfo) return undefined;
|
||||
@@ -69,19 +69,29 @@ function getNextVerificationResultPage(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function flattenVerificationResults(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
): VerificationResultReportItem[] {
|
||||
function flattenVerificationInputReportResults(
|
||||
data: InfiniteData<VerificationInputReportResponse> | undefined,
|
||||
): VerificationInputReportItem[] {
|
||||
return (
|
||||
data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function getVerificationResultTotalCount(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
loadedCount: number,
|
||||
): number {
|
||||
return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount;
|
||||
function getVerifyTypeCountingInfo(
|
||||
data: InfiniteData<VerificationInputReportResponse> | undefined,
|
||||
): VerificationInputReportTypeCountingInfo | null {
|
||||
return data?.pages[0]?.data?.verifyTypeCountingInfo ?? null;
|
||||
}
|
||||
|
||||
function formatReportItemTitle(item: VerificationInputReportItem): string {
|
||||
return item.statsNm?.trim() || item.vrfcTrgtNm?.trim() || item.statsCd;
|
||||
}
|
||||
|
||||
function formatReportItemRegion(item: VerificationInputReportItem): string {
|
||||
const parts = [item.ctpvNm, item.sggNm].filter(
|
||||
(v): v is string => Boolean(v?.trim()),
|
||||
);
|
||||
return parts.length > 0 ? parts.join(" ") : "-";
|
||||
}
|
||||
|
||||
function useVerificationReportList(
|
||||
@@ -89,28 +99,31 @@ function useVerificationReportList(
|
||||
statsCdQueryKey: string,
|
||||
vrfcMthdCd: VerificationReportTabKey,
|
||||
statsCd: string,
|
||||
enabled: boolean,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
"verificationInputReport",
|
||||
statsYr,
|
||||
vrfcMthdCd,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: ({ pageParam }) =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
verificationInputReportApi.getVerificationInputReport({
|
||||
statsYr,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
page: pageParam,
|
||||
size: PAGE_SIZE,
|
||||
prcsYnList: "0",
|
||||
vrfcMthdCd,
|
||||
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
|
||||
} satisfies VerificationResultListRequest),
|
||||
getNextPageParam: getNextVerificationResultPage,
|
||||
}),
|
||||
getNextPageParam: getNextVerificationInputReportPage,
|
||||
placeholderData: keepPreviousData,
|
||||
refetchOnWindowFocus: false,
|
||||
// 탭 전환만으로는 재조회하지 않음(명시적 새로고침/조건 변경 시만 조회)
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,28 +145,46 @@ const VerificationSelectionReport = ({
|
||||
const alert = useAlertStore((s) => s.setMessage);
|
||||
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
|
||||
const [statsCd, setStatsCd] = useState<string>("ALL");
|
||||
const [isFullReloading, setIsFullReloading] = useState(false);
|
||||
|
||||
const statsYr = String(dayjs().year() - 1);
|
||||
const statsCdQueryKey =
|
||||
statsCd === "" || statsCd === "ALL" ? "" : statsCd;
|
||||
|
||||
const handleStatsCdChange = useCallback(
|
||||
(nextStatsCd: string) => {
|
||||
setStatsCd(nextStatsCd);
|
||||
setActiveTab("NV");
|
||||
setIsFullReloading(true);
|
||||
|
||||
// 통계항목 변경 시 모든 탭 캐시 제거 후 NV부터 다시 조회
|
||||
void queryClient.removeQueries({
|
||||
queryKey: ["verificationInputReport", statsYr],
|
||||
});
|
||||
},
|
||||
[queryClient, statsYr],
|
||||
);
|
||||
|
||||
const nvQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"NV",
|
||||
statsCd,
|
||||
activeTab === "NV",
|
||||
);
|
||||
const imrQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"IMR",
|
||||
statsCd,
|
||||
activeTab === "IMR",
|
||||
);
|
||||
const hbQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"HB",
|
||||
statsCd,
|
||||
activeTab === "HB",
|
||||
);
|
||||
|
||||
const queryByTab = {
|
||||
@@ -162,44 +193,51 @@ const VerificationSelectionReport = ({
|
||||
HB: hbQuery,
|
||||
} as const;
|
||||
|
||||
const isInitialLoading =
|
||||
nvQuery.isPending || imrQuery.isPending || hbQuery.isPending;
|
||||
const isRefreshing =
|
||||
(nvQuery.isFetching && !nvQuery.isPending) ||
|
||||
(imrQuery.isFetching && !imrQuery.isPending) ||
|
||||
(hbQuery.isFetching && !hbQuery.isPending);
|
||||
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
|
||||
const activeQuery = queryByTab[activeTab];
|
||||
const isRefreshing = activeQuery.isFetching && !activeQuery.isPending;
|
||||
const error = activeQuery.error;
|
||||
|
||||
/** 통계항목 변경·최초 진입: 탭 영역 숨기고 카드 전체 로딩만 표시 */
|
||||
const hideTabsSection =
|
||||
isFullReloading ||
|
||||
(activeTab === "NV" && !nvQuery.data && (nvQuery.isPending || nvQuery.isFetching));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFullReloading) return;
|
||||
if (nvQuery.isFetching) return;
|
||||
if (nvQuery.isSuccess || nvQuery.isError) {
|
||||
setIsFullReloading(false);
|
||||
}
|
||||
}, [isFullReloading, nvQuery.isFetching, nvQuery.isSuccess, nvQuery.isError]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
const tabCodes: VerificationReportTabKey[] = ["NV", "IMR", "HB"];
|
||||
void Promise.all(
|
||||
tabCodes.map((code) =>
|
||||
queryClient.resetQueries({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
code,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, [queryClient, statsYr, statsCdQueryKey]);
|
||||
setStatsCd("ALL");
|
||||
setActiveTab("NV");
|
||||
setIsFullReloading(true);
|
||||
void queryClient.removeQueries({
|
||||
queryKey: ["verificationInputReport", statsYr],
|
||||
});
|
||||
}, [queryClient, statsYr]);
|
||||
|
||||
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
|
||||
NV: flattenVerificationResults(nvQuery.data),
|
||||
IMR: flattenVerificationResults(imrQuery.data),
|
||||
HB: flattenVerificationResults(hbQuery.data),
|
||||
const listsByTab: Record<VerificationReportTabKey, VerificationInputReportItem[]> = {
|
||||
NV: flattenVerificationInputReportResults(nvQuery.data),
|
||||
IMR: flattenVerificationInputReportResults(imrQuery.data),
|
||||
HB: flattenVerificationInputReportResults(hbQuery.data),
|
||||
};
|
||||
|
||||
const typeCountingInfo =
|
||||
getVerifyTypeCountingInfo(nvQuery.data) ??
|
||||
getVerifyTypeCountingInfo(imrQuery.data) ??
|
||||
getVerifyTypeCountingInfo(hbQuery.data);
|
||||
|
||||
const totalCountByTab: Record<VerificationReportTabKey, number> = {
|
||||
NV: getVerificationResultTotalCount(nvQuery.data, listsByTab.NV.length),
|
||||
IMR: getVerificationResultTotalCount(imrQuery.data, listsByTab.IMR.length),
|
||||
HB: getVerificationResultTotalCount(hbQuery.data, listsByTab.HB.length),
|
||||
NV: typeCountingInfo?.nvCount ?? listsByTab.NV.length,
|
||||
IMR: typeCountingInfo?.imrCount ?? listsByTab.IMR.length,
|
||||
HB: typeCountingInfo?.hbCount ?? listsByTab.HB.length,
|
||||
};
|
||||
|
||||
const openInputFromStatsCd = useCallback(
|
||||
(item: VerificationResultReportItem) => {
|
||||
(item: VerificationInputReportItem) => {
|
||||
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
|
||||
if (!nav) {
|
||||
alert(
|
||||
@@ -263,9 +301,21 @@ const VerificationSelectionReport = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const isTabContentLoading = (tab: VerificationReportTabKey) => {
|
||||
const query = queryByTab[tab];
|
||||
return (
|
||||
activeTab === tab &&
|
||||
!hideTabsSection &&
|
||||
(query.isPending ||
|
||||
(query.isFetching && !query.isFetchingNextPage) ||
|
||||
(!query.data && !query.error))
|
||||
);
|
||||
};
|
||||
|
||||
const renderResultList = (
|
||||
typeCode: VerificationReportTabKey,
|
||||
items: VerificationResultReportItem[],
|
||||
items: VerificationInputReportItem[],
|
||||
isTabLoading: boolean,
|
||||
hasNextPage: boolean,
|
||||
isFetchingNextPage: boolean,
|
||||
fetchNextPage: () => void,
|
||||
@@ -281,7 +331,13 @@ const VerificationSelectionReport = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
{items.length === 0 && !isFetchingNextPage ? (
|
||||
{isTabLoading ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : items.length === 0 && !isFetchingNextPage ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
|
||||
항목이 없습니다.
|
||||
</div>
|
||||
@@ -303,7 +359,7 @@ const VerificationSelectionReport = ({
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
{formatReportItemTitle(item)}
|
||||
</span>
|
||||
<Badge
|
||||
color={
|
||||
@@ -320,7 +376,7 @@ const VerificationSelectionReport = ({
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>{formatReportItemRegion(item)}</span>
|
||||
<span>
|
||||
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
@@ -383,25 +439,25 @@ const VerificationSelectionReport = ({
|
||||
<CodeSelectWithAll
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
onValueChange={handleStatsCdChange}
|
||||
placeholder="전체"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
|
||||
{isInitialLoading && (
|
||||
{hideTabsSection && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
{!hideTabsSection && error && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isInitialLoading && !error && (
|
||||
{!hideTabsSection && !error && (
|
||||
<section className="flex min-h-0 flex-1 flex-col">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
@@ -428,18 +484,14 @@ const VerificationSelectionReport = ({
|
||||
value={tab.value}
|
||||
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
|
||||
>
|
||||
{activeTab === tab.value
|
||||
? renderResultList(
|
||||
tab.value,
|
||||
listsByTab[tab.value],
|
||||
queryByTab[tab.value]
|
||||
.hasNextPage ?? false,
|
||||
queryByTab[tab.value]
|
||||
.isFetchingNextPage,
|
||||
queryByTab[tab.value]
|
||||
.fetchNextPage,
|
||||
)
|
||||
: null}
|
||||
{renderResultList(
|
||||
tab.value,
|
||||
listsByTab[tab.value],
|
||||
isTabContentLoading(tab.value),
|
||||
queryByTab[tab.value].hasNextPage ?? false,
|
||||
queryByTab[tab.value].isFetchingNextPage,
|
||||
queryByTab[tab.value].fetchNextPage,
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
@@ -157,7 +157,9 @@ const PublicSewageFacilityInp = () => {
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [isNotApplicable, setIsNotApplicable] = useState(false);
|
||||
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
@@ -175,6 +177,19 @@ const PublicSewageFacilityInp = () => {
|
||||
const [mainFacilityMappingSaveDialogKey, setMainFacilityMappingSaveDialogKey] = useState(0);
|
||||
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const isNotApplicable = isNotApplicableByTab.get('ST00060001') ?? false;
|
||||
const setIsNotApplicable: Dispatch<SetStateAction<boolean>> = (next) => {
|
||||
setIsNotApplicableByTab((prev) => {
|
||||
const current = prev.get('ST00060001') ?? false;
|
||||
const resolved =
|
||||
typeof next === 'function'
|
||||
? (next as (prevState: boolean) => boolean)(current)
|
||||
: next;
|
||||
const nextMap = new Map(prev);
|
||||
nextMap.set('ST00060001', resolved);
|
||||
return nextMap;
|
||||
});
|
||||
};
|
||||
|
||||
const periodEnterableRef = useRef(true);
|
||||
const searchDoneRef = useRef(false);
|
||||
@@ -195,7 +210,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
@@ -276,15 +291,15 @@ const PublicSewageFacilityInp = () => {
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && submitList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
|
||||
const requestData = {
|
||||
@@ -515,19 +530,19 @@ const PublicSewageFacilityInp = () => {
|
||||
if (!(await confirm('저장 하시겠습니까?'))) return;
|
||||
|
||||
if (activeTab === 'ST00060001') {
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
if (!treatAsNoApplicable && saveList.length === 0) {
|
||||
alert('해당사항 없음을 체크해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const param: PublicSewageFacilitySaveRequest = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
ctpvNm: lastSearchedValues.sido ?? '',
|
||||
@@ -655,8 +670,6 @@ const PublicSewageFacilityInp = () => {
|
||||
param: PublicSewageFacilityRequestParams,
|
||||
searchContext: SearchFormValues | null,
|
||||
): Promise<boolean> => {
|
||||
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
|
||||
setIsNotApplicable(false);
|
||||
try {
|
||||
if (tabKey === 'ST00060001') {
|
||||
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
|
||||
@@ -856,6 +869,11 @@ const PublicSewageFacilityInp = () => {
|
||||
setLastSearchedValues(values);
|
||||
searchDoneRef.current = true;
|
||||
setHasSearched(true);
|
||||
setIsNotApplicableByTab((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set('ST00060001', false);
|
||||
return next;
|
||||
});
|
||||
|
||||
setIsLoading(true);
|
||||
const param = buildPublicSewageFacilityParams(values);
|
||||
@@ -870,6 +888,19 @@ const PublicSewageFacilityInp = () => {
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
const prevTabKey = activeTabRef.current;
|
||||
if (prevTabKey === 'ST00060001') {
|
||||
const latestRowsInPrevTab =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (latestRowsInPrevTab.length > 0 || rowDataMap.has('ST00060001')) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set('ST00060001', latestRowsInPrevTab);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activeTabRef.current = tabKey;
|
||||
setActiveTab(tabKey);
|
||||
|
||||
@@ -885,7 +916,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const handleReset = () => {
|
||||
setIsLoading(false);
|
||||
setHasSearched(false);
|
||||
setIsNotApplicable(false);
|
||||
setIsNotApplicableByTab(new Map());
|
||||
setShowIsNotApplicable(false);
|
||||
|
||||
setRowDataMap(new Map());
|
||||
@@ -1215,7 +1246,7 @@ const PublicSewageFacilityInp = () => {
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
key={tab.tabKey}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
|
||||
@@ -1278,6 +1309,11 @@ const PublicSewageFacilityInp = () => {
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionResetSubmitted={async () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
if (!ctx || !searchParamRef.current) return;
|
||||
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
|
||||
}}
|
||||
/>
|
||||
<SubmitDailog
|
||||
open={showDailog}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user