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

This commit is contained in:
kiminchan
2026-05-15 15:06:46 +09:00
27 changed files with 551 additions and 51 deletions
@@ -0,0 +1,9 @@
export const dynamic = "force-dynamic";
import { ContentsPage } from "@/components/ui/contents";
export default async function Contents() {
const contsSn = 28;
const menuCd = "24";
return <ContentsPage contsSn={contsSn} menuCd={menuCd} />;
}
@@ -1,3 +1,6 @@
/**
* 물 재이용 사례
*/
import SubPageHeader from "@/components/SubPageHeader";
import BusinessSubNav, { type BusinessSubNavMenu } from "@/components/BusinessSubNav";
@@ -0,0 +1,123 @@
"use client";
import React from "react";
import Image, { type StaticImageData } from "next/image";
import lawPlan from "@/public/images/landing/intro/law_timeline.png";
import planPerformance from "@/public/images/landing/intro/performance_left.png";
import domesticChange from "@/public/images/landing/intro/domestic_textbox.png";
import policyHistory from "@/public/images/landing/intro/policy_timeline.png";
import lawRelation from "@/public/images/landing/intro/rel_note.png";
import domesticLinkage from "@/public/images/landing/intro/linkage.png";
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function ImgCard({ src, alt }: { src: StaticImageData; alt: string }) {
return (
<div className="w-full overflow-x-auto">
<Image
src={src}
alt={alt}
className="mx-auto block max-w-none"
style={{ width: "auto", height: "auto" }}
/>
</div>
);
}
function SectionShell({
id,
title,
children,
className = "",
}: {
id: string;
title: string;
children: React.ReactNode;
className?: string;
}) {
return (
<section id={id} className={`scroll-mt-[120px] ${className}`}>
<SectionTitle>{title}</SectionTitle>
<div className="mt-8">{children}</div>
</section>
);
}
export default function LawIntroSection() {
const title = "물재이용법";
return (
<div className="bg-white">
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
<div className="mt-10">
<SectionShell
id="law-plan"
title="법률 제·개정 및 기본계획 수립"
className="mt-10"
>
<ImgCard src={lawPlan} alt="법률 제·개정 및 기본계획 수립" />
</SectionShell>
<Hr className="my-11" />
<SectionShell
id="plan-performance"
title="제2차 계획 대비 실적 현황"
>
<ImgCard src={planPerformance} alt="제2차 계획 대비 실적 현황" />
</SectionShell>
<Hr className="my-11" />
<SectionShell id="domestic-change" title="우리나라의 수자원 현황 및 물이용 여건">
<ImgCard src={domesticChange} alt="우리나라의 수자원 현황 및 물이용 여건" />
</SectionShell>
<Hr className="my-11" />
<SectionShell
id="policy-history"
title="물 재이용 정책 변화 추이"
>
<ImgCard src={policyHistory} alt="물 재이용 정책 변화 추이" />
</SectionShell>
<Hr className="my-11" />
<SectionShell
id="law-relation"
title="타법령과의 관계 검토"
>
<ImgCard src={lawRelation} alt="타법령과의 관계 검토" />
</SectionShell>
<Hr className="my-11" />
<SectionShell
id="domestic-linkage"
title="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요"
>
<ImgCard src={domesticLinkage} alt="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요" />
</SectionShell>
</div>
</div>
);
}
@@ -287,7 +287,7 @@ export default function SewageStatusSection() {
const isSubItem = type === "red-item" || type === "blue-item";
if (index === 0) {
console.log(header.id, value);
// console.log(header.id, value);
return (
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} relative`}>
{isSubItem ? (
+1 -1
View File
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
id: "status-treated-usage",
label: "연도별 하수처리수 재이용 현황",
label: "용도별 하수처리수 재이용 현황",
},
{
id: "status-treated-facility",
+1 -1
View File
@@ -16,7 +16,7 @@ export default function RootLayout({
<title>물재이용 대민</title>
</head>
<body>
<div className="min-w-[1400px] overflow-x-auto">
<div className="min-w-[1400px]">
{children}
</div>
</body>
-2
View File
@@ -1,12 +1,10 @@
// app/main/layout.tsx
import "../reuse.module.css";
import TopNav from "@/components/TopNav";
import { StoreProvider } from "../store";
export default function ReuseLandingLayout({ children }: { children: React.ReactNode }) {
return (
<StoreProvider>
<TopNav />
<div className="pt-[83px]">{children}</div> {/* TopNav 높이만큼 */}
</StoreProvider>
);
+1 -1
View File
@@ -33,7 +33,7 @@ export const StoreZus = (
return createStore<DmStore>()((set) => {
if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", process.env.NEXT_PUBLIC_SITE_ID||"DM");
tokenManager.setToken("site-theme-key", "green");
tokenManager.setToken("site-theme-key", "white");
}
return {
+59 -33
View File
@@ -1,3 +1,6 @@
/**
* 우측 메뉴
*/
"use client";
import Link from "next/link";
@@ -47,44 +50,67 @@ export default function BusinessSubNav({
setOpenMenus(initialOpenState);
}, [menus]);
useEffect(() => {
if (!allSections.length) return;
useEffect(() => {
if (!allSections.length) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
const targets = allSections
.map((section) => document.getElementById(section.id))
.filter(Boolean) as HTMLElement[];
if (visible[0]) {
setActiveId(visible[0].target.id);
}
},
{
rootMargin: `-${offsetTop}px 0px -55% 0px`,
threshold: [0.2, 0.35, 0.5, 0.7],
if (!targets.length) return;
let rafId = 0;
const updateActiveSection = () => {
const baseLine = offsetTop + 500;
let nextActiveId = targets[0]?.id ?? "";
for (const el of targets) {
const { top } = el.getBoundingClientRect();
if (top <= baseLine) {
nextActiveId = el.id;
continue;
}
);
const targets = allSections
.map((section) => document.getElementById(section.id))
.filter(Boolean) as HTMLElement[];
break;
}
targets.forEach((el) => observer.observe(el));
setActiveId((prev) => (prev === nextActiveId ? prev : nextActiveId));
};
const hash = window.location.hash.replace("#", "");
const initialActive =
allSections.find((section) => section.id === hash)?.id ??
allSections[0]?.id ??
"";
const requestUpdate = () => {
if (rafId) return;
setActiveId(initialActive);
rafId = window.requestAnimationFrame(() => { // 예약번호 반환
updateActiveSection();
rafId = 0;
});
};
return () => observer.disconnect();
}, [allSections, offsetTop]);
const hash = window.location.hash.replace("#", "");
const initialActive =
allSections.find((section) => section.id === hash)?.id ??
allSections[0]?.id ??
"";
useEffect(() => {
if (!activeId) return;
setActiveId(initialActive);
updateActiveSection();
window.addEventListener("scroll", requestUpdate, { passive: true });
window.addEventListener("resize", requestUpdate);
return () => {
window.removeEventListener("scroll", requestUpdate);
window.removeEventListener("resize", requestUpdate);
if (rafId) {
window.cancelAnimationFrame(rafId);
}
};
}, [allSections, offsetTop]);
useEffect(() => {
if (!activeId) return;
const parentMenu = menus.find((menu) =>
(menu.sections ?? []).some((section) => section.id === activeId)
@@ -93,13 +119,13 @@ export default function BusinessSubNav({
if (!parentMenu) return;
setOpenMenus(() => {
const nextOpenState: Record<string, boolean> = {};
const nextOpenState: Record<string, boolean> = {};
menus.forEach((menu) => {
menus.forEach((menu) => {
nextOpenState[menu.key] = menu.key === parentMenu.key;
});
});
return nextOpenState;
return nextOpenState;
});
}, [activeId, menus]);
@@ -0,0 +1,26 @@
// apps/dm/src/entities/content/
// contents-api.ts
import { unstable_cache } from "next/cache";
import { ENDPOINT } from "@/src/shared/config";
export const getCachedContentDetail = unstable_cache(
async (contsSn: number, menuCd: string) => {
const baseUrl = process.env.NEXT_PUBLIC_PROXY_HOST || "http://localhost:3015";
const res = await fetch(
`${baseUrl}${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}?menuCd=${menuCd}`,
{
next: { revalidate: 3600, tags: [`content:${contsSn}`] },
}
);
if (!res.ok) {
console.log(res);
throw new Error("Failed to fetch content");
}
return res.json();
},
["dm-content-detail"],
);
@@ -0,0 +1,18 @@
/**
* 컨텐츠 서버 페이지
*/
import { getCachedContentDetail } from "./contents-api";
import { ContentsPageClient } from "./contentsPageClient";
type ContentsPageProps = {
contsSn: number;
menuCd: string;
};
async function ContentsPage({ contsSn, menuCd }: ContentsPageProps) {
const initialContent = await getCachedContentDetail(contsSn, menuCd);
return <ContentsPageClient initialContent={initialContent} />;
}
export { ContentsPage };
@@ -0,0 +1,37 @@
"use client";
import { useEffect, useRef } from "react";
import type { ContentsDetail } from "@/src/entities/content/model/contents-interface";
type ContentsPageClientProps = {
initialContent: ContentsDetail;
};
function ContentsPageClient({ initialContent }: ContentsPageClientProps) {
const shadowRootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!shadowRootRef.current || !initialContent?.contsCn) return;
const shadowRoot =
shadowRootRef.current.shadowRoot ??
shadowRootRef.current.attachShadow({ mode: "open" });
shadowRoot.innerHTML = "";
const styleElement = document.createElement("style");
styleElement.textContent = `div { font-size: 16px; line-height: 1.5; }`;
shadowRoot.appendChild(styleElement);
const contentElement = document.createElement("div");
contentElement.innerHTML = initialContent.contsCn;
shadowRoot.appendChild(contentElement);
}, [initialContent]);
return (
<div className="card card-border !mt-6 !p-6">
<div ref={shadowRootRef} />
</div>
);
}
export { ContentsPageClient };
+1
View File
@@ -0,0 +1 @@
export { ContentsPage } from "./contentsPage";
+1 -1
View File
@@ -60,7 +60,7 @@ export function middleware(req: NextRequest) {
if (pathname.startsWith("/user-service")
|| pathname.startsWith("/cms-service")) {
let queryParams = searchParams.toString();
const queryParams = searchParams.toString();
const requestHeaders = new Headers(req.headers);
const newUrl = `${serverApiUrl}${pathname}${queryParams ? `?${queryParams}` : ""}`;
+4
View File
@@ -48,6 +48,10 @@ const nextConfig: NextConfig = {
source: "/operation-service/:path*",
destination: `${subDomain}/operation-service/:path*`,
},
{
source: "/cms-service/:path*",
destination: `${subDomain}/cms-service/:path*`,
},
],
fallback: [],
};
@@ -0,0 +1,101 @@
import { client } from "@/src/shared/api";
import { ENDPOINT } from "@/src/shared/config";
import type {
ContentsService,
ContentsParams,
ContentsList,
ContentsFormData,
ContentsVersionParams,
ContentsVersionList,
ContentsDetail,
} from "../model/contents-interface";
export class ContentsApiService implements ContentsService {
public async getContentsList(params: ContentsParams): Promise<ContentsList> {
const response = await client.get<ContentsList>(
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
{ params },
);
return response.data;
}
public async getLatestApprovedContentsInfo(
contsSn: number,
menuCd: string,
): Promise<ContentsDetail> {
const response = await client.get<ContentsDetail>(
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
{ params: { menuCd } },
);
return response.data;
}
public async getContentsVersionList(
params: ContentsVersionParams,
): Promise<ContentsVersionList> {
const response = await client.get<ContentsVersionList>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
{ params },
);
return response.data;
}
public async getLatestContentsInfo(
contsSn: number,
contsVer: number,
): Promise<ContentsDetail> {
const response = await client.get<ContentsDetail>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
);
return response.data;
}
public async createContents(data: ContentsFormData): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
data,
);
return response.data;
}
public async updateContents(
contsSn: number,
contsVer: number,
data: ContentsFormData,
): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
data,
);
return response.data;
}
public async deleteContents(
contsSn: number,
contsVer: number,
): Promise<void> {
await client.delete(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
);
}
public async requestApprove(
contsSn: number,
contsVer: number,
): Promise<void> {
await client.put(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`,
);
}
public async approveContents(
contsSn: number,
contsVer: number,
): Promise<void> {
await client.put(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
);
}
}
export const ContentsApi = new ContentsApiService();
+8
View File
@@ -0,0 +1,8 @@
export { ContentsApi } from "./api/contents-service";
export {
CONTENTS_APPROVE_STATUS,
contentsFormSchema,
type Contents,
type ContentsParams,
type ContentsFormData,
} from "./model/contents-interface";
@@ -0,0 +1,34 @@
interface SortConfig {
empty: boolean;
sorted: boolean;
unsorted: boolean;
}
interface PageableConfig {
sort: SortConfig;
offset: number;
pageNumber: number;
pageSize: number;
paged: boolean;
unpaged: boolean;
}
export interface PageableList<T> {
content: T[];
pageable: PageableConfig;
sort: SortConfig;
totalPages: number;
totalElements: number;
number: number;
numberOfElements: number;
size: number;
first: boolean;
last: boolean;
empty: boolean;
}
export interface PageableParams {
page?: number;
size?: number;
sort?: string;
}
@@ -0,0 +1,105 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "./api-interface";
export const CONTENTS_APPROVE_STATUS = {
DRAFT: "01",
REQUESTED: "02",
APPROVED: "99",
} as const;
export type ContentsApproveStatus =
(typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS];
export interface ContentsParams extends PageableParams {
contsTtl?: string;
aprvContsVer?: number;
useYn?: string;
}
export interface ContentsVersionParams extends PageableParams {
contsSn?: number;
contsCn?: string;
aprvCd?: ContentsApproveStatus;
cclCd?: string;
koglCd?: string;
tag?: string;
useYn?: string;
}
export interface ContentsVersion {
contsSn: number;
contsVer: number;
contsCn: string;
useYn: string;
}
export const contentsSchema = z.object({
contsSn: z.number(),
contsTtl: z.string(),
aprvContsVer: z.number(),
useYn: z.string(),
mdfcnDt: z.string(),
mdfr: z.string(),
mdfrNm: z.string(),
aprvDt: z.string(),
autzrNm: z.string(),
workAprvCd: z.string(),
workAprvNm: z.string(),
workContsVer: z.number(),
});
export const contentsDetailSchema = z.object({
contsSn: z.number(),
contsVer: z.number(),
contsTtl: z.string(),
contsCn: z.string(),
contsRmrk: z.string().nullish(),
aprvCd: z.enum([
CONTENTS_APPROVE_STATUS.DRAFT,
CONTENTS_APPROVE_STATUS.REQUESTED,
CONTENTS_APPROVE_STATUS.APPROVED,
]),
cclCd: z.string().nullish(),
koglCd: z.string().nullish(),
tag: z.string().nullish(),
aprvDt: z.string(),
autzr: z.string(),
useYn: z.string(),
menuPicUserUnqIdNm: z.string().nullish(),
});
export const contentsFormSchema = contentsDetailSchema.omit({
contsVer: true,
aprvDt: true,
autzr: true,
});
export type Contents = z.infer<typeof contentsSchema>;
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
export type ContentsFormData = z.infer<typeof contentsFormSchema>;
export type ContentsList = PageableList<Contents>;
export type ContentsVersionList = PageableList<ContentsVersion>;
export interface ContentsService {
getContentsList(params: ContentsParams): Promise<ContentsList>;
getLatestApprovedContentsInfo(
contsSn: number,
menuCd: string,
): Promise<ContentsDetail>;
getContentsVersionList(
params: ContentsVersionParams,
): Promise<ContentsVersionList>;
getLatestContentsInfo(
contsSn: number,
contsVer: number,
): Promise<ContentsDetail>;
createContents(data: ContentsFormData): Promise<void>;
updateContents(
contsSn: number,
contsVer: number,
data: ContentsFormData,
): Promise<void>;
deleteContents(contsSn: number, contsVer: number): Promise<void>;
requestApprove(contsSn: number, contsVer: number): Promise<void>;
approveContents(contsSn: number, contsVer: number): Promise<void>;
}
@@ -6,6 +6,7 @@ import type { customHtdflBdst } from "@/app/store/ruseFcltMng/fcltReg/htdflReg";
import { formatArchArea, tableData } from "@/components/lib/func/common";
import HtdflRegForm from "@/components/ui/form/fcltReg/htdflReg/htdflRegForm";
import { BdstReg } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import {
ContentsDiv,
ScrollArea,
@@ -141,7 +142,7 @@ export default function Home() {
<UiTable
tableData={bdstTable || []}
headlist={htdfl.upHeadList}
pageSize={20}
pageSize={DEFAULT_LIST_SIZE}
total={htdflResCount}
headName={"시설물 목록"}
cellClick={(val) => {
@@ -15,6 +15,7 @@
* */
"use client";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import {
ContentsDiv,
ScrollArea,
@@ -353,7 +354,7 @@ export default function PublicReuseFaciltyInsert() {
<UiTable
tableData={bdstTable || []}
headlist={pubFacHeadList}
pageSize={10}
pageSize={DEFAULT_LIST_SIZE}
total={pbnstHasuResCount || 20}
headName={"공공 하수처리시설 목록"}
cellClick={cellClick}
@@ -1,5 +1,6 @@
import { getFetchWithCount, postFectch, putFectch, deleteFectch } from "@/components/lib/api";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import type { TableUpperProps } from "@/app/store/common/type/header";
import { z } from "zod";
import { makeMdfr, makeRstr } from "../bdstReg";
@@ -121,7 +122,7 @@ export const htdflFacStateExport: htdflFacState = {
const initFetch = async (code: string) => {
let url = `/drain/facilityList/1/11`
let url = `/drain/facilityList/1/${DEFAULT_LIST_SIZE}`
url+=`/${code}`
@@ -137,7 +138,7 @@ export const htdflFacActionsExport: (set: any) => htdflFacSActions = (
try {
const htdflRes = await getFetchWithCount(
"wr",
`/drain/facilityList/${pageNation}/11/${code}`
`/drain/facilityList/${pageNation}/${DEFAULT_LIST_SIZE}/${code}`
);
return set((state: htdflFacState) => ({
...state,
@@ -181,7 +182,7 @@ export const htdflFacActionsExport: (set: any) => htdflFacSActions = (
}
const htdflRes = await getFetchWithCount(
"wr",
`/drain/facilityList/1/11/${code}`
`/drain/facilityList/1/${DEFAULT_LIST_SIZE}/${code}`
);
set((state: htdflFacState) => ({
...state,
@@ -251,7 +252,7 @@ export const htdflFacActionsExport: (set: any) => htdflFacSActions = (
},
htdflNmSearch :async(code:string | number, fcltNm:string) =>{
try {
const htdflRes = await getFetchWithCount("wr", `/drain/facilityList/1/11/${code}/${fcltNm}`);
const htdflRes = await getFetchWithCount("wr", `/drain/facilityList/1/${DEFAULT_LIST_SIZE}/${code}/${fcltNm}`);
set((state: htdflFacState) => {
return {
...state,
@@ -6,7 +6,7 @@ import {
postFectch,
putFectch,
} from "@/components/lib/api";
import { SITE_ID } from "@/src/shared/config";
import { DEFAULT_LIST_SIZE, SITE_ID } from "@/src/shared/config";
import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList";
import {
type customPbnst,
@@ -113,7 +113,7 @@ const hasuListFetch = async (
) => {
const checkWrRegListRes = await checkWrRegList(); // 재사용시설에 대한 정보 조회
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/11/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`;
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/${DEFAULT_LIST_SIZE}/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`;
const data = checkWrRegListRes.filter((item: string) => item !== "");
if (searchTxt) {
@@ -6,6 +6,7 @@ import {
postFectch,
putFectch,
} from "@/components/lib/api";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import { z } from "zod";
import { makeMdfr, makeRstr } from "../bdstReg";
import {
@@ -14,7 +15,7 @@ import {
validationMessages,
} from "./type";
export const TEST_UUID = "2d4b151d-1828-41e0-91d9-4cd28ea1cc7e";
export const rainRegSize = 15;
export const rainRegSize = DEFAULT_LIST_SIZE;
/**
* 밧물이용시설 등록 (스토어)
@@ -1,6 +1,7 @@
import { deleteFectch, getFetchWithCount, postFectch, putFectch } from "@/components/lib/api";
import { upHeadList, type chgBuilding } from "../rainReg";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import type { TableUpperProps } from "@/app/store/common/type/header";
import { z } from "zod";
import { makeMdfr, makeRstr } from "../bdstReg";
@@ -14,7 +15,7 @@ import { baseReuseBuildingSchema, chgBuildingSchema, validationMessages } from "
* 재이용시설 등록 - 중수도시설 등록
* 중수도시설에서 사용하는 api 및 타입 등을 관리한다.
* */
export const rswtRegSize =15;
export const rswtRegSize = DEFAULT_LIST_SIZE;
const rswtrObjSchema = z.object({
bdstRuseFcltSn: z.number().nullish(),
orgwtKndCd: z.string({ required_error: validationMessages.requiredSelect("원수 종류") }).min(1, { message: validationMessages.requiredSelect("원수 종류") }),
+2
View File
@@ -1,2 +1,4 @@
export * from "./api";
export * from "./env";
export const DEFAULT_LIST_SIZE = 10;
@@ -152,7 +152,7 @@ export function UiTable<T extends object = Record<string, unknown>>({
<></>
)}
<div className={`card !p-0r !m-[1.2rem] ${className}`}>
<Table variant="secondary" className={widthFixed ? "table-fixed" : "w-max min-w-max"}>
<Table variant="secondary" className={widthFixed ? "table-fixed" : ""}>
{widthFixed ?
<colgroup>
{leafCols.map(col => (