Compare commits
8
Commits
39dca810a5
...
f816bf005e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f816bf005e | ||
|
|
791ad6ce37 | ||
|
|
4bc802cee2 | ||
|
|
4662c8c896 | ||
|
|
7058560a03 | ||
|
|
79a5999ad5 | ||
|
|
1aeded5da2 | ||
|
|
e8d1d882c2 |
@@ -4,7 +4,7 @@ import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "행정지원시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "행정지원시스템",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Providers } from "@/app/providers";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "디지털 행정지원 시스템",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Image from "next/image";
|
||||
import "@/app/styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 행정지원 시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export function BaseLayoutWrapper({ children }: BaseLayoutWrapperProps) {
|
||||
authService={authService}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
sidebarTriggerPlacement="lnb"
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -53,21 +53,52 @@ export function createFileVrfcSplmntColumns() {
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("fileSeComCd", {
|
||||
header: () => <p className="whitespace-nowrap">파일유형</p>,
|
||||
meta: { align: "left" },
|
||||
header: "파일유형",
|
||||
// 고정 너비 + 자동 줄바꿈 (긴 텍스트로 표가 가로로 늘어나지 않도록)
|
||||
meta: { align: "left", width: 100 },
|
||||
cell: ({ getValue }) => (
|
||||
<p>{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}</p>
|
||||
<p className="whitespace-normal break-words">
|
||||
{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}
|
||||
</p>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("fileSeNm", {
|
||||
header: () => <p className="whitespace-nowrap">파일구분</p>,
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => <p className="whitespace-nowrap">{formatOptional(getValue())}</p>,
|
||||
header: "파일구분",
|
||||
meta: { align: "left", width: 320 },
|
||||
// 표 레이아웃과 무관하게 셀 내용 너비를 320px 로 고정하고 넘치는 텍스트는 자동 줄바꿈
|
||||
cell: ({ getValue }) => (
|
||||
<p
|
||||
style={{
|
||||
width: 320,
|
||||
maxWidth: 320,
|
||||
// 검증내역과 글자가 붙어 보이지 않도록 오른쪽 여백
|
||||
paddingRight: 12,
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "normal",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{formatOptional(getValue())}
|
||||
</p>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("vrfcRsltCn", {
|
||||
header: "검증내역",
|
||||
// 너비 미지정: 나머지 영역을 사용, 넘치는 텍스트는 자동 줄바꿈
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
cell: ({ getValue }) => (
|
||||
<p
|
||||
style={{
|
||||
// 파일구분과 글자가 붙어 보이지 않도록 왼쪽 여백
|
||||
paddingLeft: 12,
|
||||
whiteSpace: "normal",
|
||||
wordBreak: "normal",
|
||||
overflowWrap: "anywhere",
|
||||
}}
|
||||
>
|
||||
{formatOptional(getValue())}
|
||||
</p>
|
||||
),
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ function FileVrfcSplmntDialog({
|
||||
},
|
||||
dataTableOptions: {
|
||||
caption: "파일보완 목록으로 선택, 파일유형, 파일구분, 검증내역 정보 제공",
|
||||
// 열 너비를 고정해 긴 텍스트는 셀 안에서 줄바꿈
|
||||
className: "table-fixed",
|
||||
viewPagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -86,7 +86,13 @@ function ChecklistSection({
|
||||
>
|
||||
<TabsList>
|
||||
{visibleReviewSteps.map((stepCode) => (
|
||||
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
||||
<TabsTrigger
|
||||
key={stepCode}
|
||||
value={stepCode}
|
||||
id={`fnrs-cklst-step-tab-${stepCode}`}
|
||||
aria-controls="fnrs-cklst-task-tabs"
|
||||
className="!px-6 !py-3 text-xl"
|
||||
>
|
||||
{CHECKLIST_TAB_LABELS[stepCode]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -96,6 +102,7 @@ function ChecklistSection({
|
||||
|
||||
{taskCodes.length > 0 ? (
|
||||
<Tabs
|
||||
id="fnrs-cklst-task-tabs"
|
||||
value={selectedTaskCode ?? taskCodes[0]}
|
||||
onValueChange={setSelectedTaskCode}
|
||||
className="w-full"
|
||||
@@ -107,6 +114,8 @@ function ChecklistSection({
|
||||
<TabsTrigger
|
||||
key={code}
|
||||
value={code}
|
||||
id={`fnrs-cklst-tab-${code}`}
|
||||
aria-controls={`fnrs-cklst-panel-${code}`}
|
||||
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||
>
|
||||
{TASK_CODE_LABELS[code] ?? code}
|
||||
@@ -120,7 +129,15 @@ function ChecklistSection({
|
||||
</div>
|
||||
|
||||
{taskCodes.map((code) => (
|
||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
||||
<TabsContent
|
||||
key={code}
|
||||
value={code}
|
||||
id={`fnrs-cklst-panel-${code}`}
|
||||
aria-labelledby={`fnrs-cklst-tab-${code}`}
|
||||
className="mt-0 border-0 p-0 shadow-none ring-0"
|
||||
>
|
||||
{/* 선택된 탭(검토단계·업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||
<h3 className="sr-only">{CHECKLIST_TAB_LABELS[selectedReviewStep]} {TASK_CODE_LABELS[code] ?? code} 체크리스트</h3>
|
||||
<ChecklistTaskPanel
|
||||
selectedReviewStep={selectedReviewStep}
|
||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||
|
||||
@@ -85,7 +85,13 @@ function ChecklistSection({
|
||||
>
|
||||
<TabsList>
|
||||
{visibleReviewSteps.map((stepCode) => (
|
||||
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
||||
<TabsTrigger
|
||||
key={stepCode}
|
||||
value={stepCode}
|
||||
id={`instl-cklst-step-tab-${stepCode}`}
|
||||
aria-controls="instl-cklst-task-tabs"
|
||||
className="!px-6 !py-3 text-xl"
|
||||
>
|
||||
{CHECKLIST_TAB_LABELS[stepCode]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -95,6 +101,7 @@ function ChecklistSection({
|
||||
|
||||
{taskCodes.length > 0 ? (
|
||||
<Tabs
|
||||
id="instl-cklst-task-tabs"
|
||||
value={selectedTaskCode ?? taskCodes[0]}
|
||||
onValueChange={setSelectedTaskCode}
|
||||
className="w-full"
|
||||
@@ -106,6 +113,8 @@ function ChecklistSection({
|
||||
<TabsTrigger
|
||||
key={code}
|
||||
value={code}
|
||||
id={`instl-cklst-tab-${code}`}
|
||||
aria-controls={`instl-cklst-panel-${code}`}
|
||||
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||
>
|
||||
{TASK_CODE_LABELS[code] ?? code}
|
||||
@@ -119,7 +128,15 @@ function ChecklistSection({
|
||||
</div>
|
||||
|
||||
{taskCodes.map((code) => (
|
||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
||||
<TabsContent
|
||||
key={code}
|
||||
value={code}
|
||||
id={`instl-cklst-panel-${code}`}
|
||||
aria-labelledby={`instl-cklst-tab-${code}`}
|
||||
className="mt-0 border-0 p-0 shadow-none ring-0"
|
||||
>
|
||||
{/* 선택된 탭(검토단계·업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||
<h3 className="sr-only">{CHECKLIST_TAB_LABELS[selectedReviewStep]} {TASK_CODE_LABELS[code] ?? code} 체크리스트</h3>
|
||||
<ChecklistTaskPanel
|
||||
selectedReviewStep={selectedReviewStep}
|
||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||
|
||||
@@ -86,7 +86,13 @@ function ChecklistSection({
|
||||
>
|
||||
<TabsList>
|
||||
{visibleReviewSteps.map((stepCode) => (
|
||||
<TabsTrigger key={stepCode} value={stepCode} className="!px-6 !py-3 text-xl">
|
||||
<TabsTrigger
|
||||
key={stepCode}
|
||||
value={stepCode}
|
||||
id={`sewer-cklst-step-tab-${stepCode}`}
|
||||
aria-controls="sewer-cklst-task-tabs"
|
||||
className="!px-6 !py-3 text-xl"
|
||||
>
|
||||
{CHECKLIST_TAB_LABELS[stepCode]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -96,6 +102,7 @@ function ChecklistSection({
|
||||
|
||||
{taskCodes.length > 0 ? (
|
||||
<Tabs
|
||||
id="sewer-cklst-task-tabs"
|
||||
value={selectedTaskCode ?? taskCodes[0]}
|
||||
onValueChange={setSelectedTaskCode}
|
||||
className="w-full"
|
||||
@@ -107,6 +114,8 @@ function ChecklistSection({
|
||||
<TabsTrigger
|
||||
key={code}
|
||||
value={code}
|
||||
id={`sewer-cklst-tab-${code}`}
|
||||
aria-controls={`sewer-cklst-panel-${code}`}
|
||||
className="!px-6 !py-3 text-xl [&:after]:hidden"
|
||||
>
|
||||
{TASK_CODE_LABELS[code] ?? code}
|
||||
@@ -120,7 +129,15 @@ function ChecklistSection({
|
||||
</div>
|
||||
|
||||
{taskCodes.map((code) => (
|
||||
<TabsContent key={code} value={code} className="mt-0 border-0 p-0 shadow-none ring-0">
|
||||
<TabsContent
|
||||
key={code}
|
||||
value={code}
|
||||
id={`sewer-cklst-panel-${code}`}
|
||||
aria-labelledby={`sewer-cklst-tab-${code}`}
|
||||
className="mt-0 border-0 p-0 shadow-none ring-0"
|
||||
>
|
||||
{/* 선택된 탭(검토단계·업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||
<h3 className="sr-only">{CHECKLIST_TAB_LABELS[selectedReviewStep]} {TASK_CODE_LABELS[code] ?? code} 체크리스트</h3>
|
||||
<ChecklistTaskPanel
|
||||
selectedReviewStep={selectedReviewStep}
|
||||
selectedRows={getChecklistDisplayRows(cklstResultList, code)}
|
||||
|
||||
@@ -190,7 +190,13 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
{checklistTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.code} value={tab.code} className="!px-6 !py-3 text-xl">
|
||||
<TabsTrigger
|
||||
key={tab.code}
|
||||
value={tab.code}
|
||||
id={`pbadms-cklst-tab-${tab.code}`}
|
||||
aria-controls={`pbadms-cklst-panel-${tab.code}`}
|
||||
className="!px-6 !py-3 text-xl"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
@@ -198,7 +204,15 @@ function ChecklistFormSection({ docSeComCd, fieldPrefix }: ChecklistFormSectionP
|
||||
|
||||
{checklistTabs.map((tab) => {
|
||||
return (
|
||||
<TabsContent key={tab.code} value={tab.code} className="space-y-4">
|
||||
<TabsContent
|
||||
key={tab.code}
|
||||
value={tab.code}
|
||||
id={`pbadms-cklst-panel-${tab.code}`}
|
||||
aria-labelledby={`pbadms-cklst-tab-${tab.code}`}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* 선택된 탭(업무구분)의 패널임을 알 수 있도록 숨김 제목 제공 */}
|
||||
<h3 className="sr-only">{tab.label} 체크리스트</h3>
|
||||
<Table caption={`${tab.label} 체크리스트 목록으로 검토항목, 주요 검토내용, 검토 결과 정보 제공`} variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "유역하수도 포탈",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "유역하수도 포탈",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -5,7 +5,7 @@ import "@/app/styles/base.css";
|
||||
import "@/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "유역하수도 포탈",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "하수도플랫폼 통합관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "하수도플랫폼 통합관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -5,7 +5,7 @@ import "@/app/styles/base.css";
|
||||
import "@/app/styles/components.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "하수도플랫폼 통합관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/src/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "@/src/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "하수처리시설 운영실태",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "하수처리시설 운영실태 분석시스템입니다",
|
||||
keywords: ["운영실태"],
|
||||
openGraph: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "../../../src/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -20,7 +20,7 @@ import "../src/app/styles/components.css";
|
||||
import "../src/app/styles/grid-ellipsis.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "디지털 하수관로",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "디지털 하수관로 시스템입니다",
|
||||
keywords: ["하수관로"],
|
||||
openGraph: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "하수도통계 관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "@/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "하수도통계 관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -4,7 +4,7 @@ import Image from "next/image";
|
||||
import "@/app/styles/base.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "하수도통계 관리시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@/src/app/styles/base.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "유역하수도 통합 정보플랫폼",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
};
|
||||
|
||||
export default function AuthLayout({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import { App } from "../src/app";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "물재이용매칭시스템",
|
||||
title: "유역하수도통합플랫폼 Hasuon",
|
||||
description: "물재이용매칭 시스템입니다",
|
||||
keywords: ["물재이용매칭"],
|
||||
openGraph: {
|
||||
|
||||
@@ -136,11 +136,14 @@ export function Header({
|
||||
authService,
|
||||
tokenManager,
|
||||
viewOnly,
|
||||
showSidebarTrigger = true,
|
||||
}: {
|
||||
authService?: AuthService;
|
||||
/** @deprecated authService를 사용하세요. */
|
||||
tokenManager?: TokenManager;
|
||||
viewOnly?: boolean;
|
||||
/** false 이면 LNB 토글 버튼을 헤더에 렌더링하지 않고 같은 크기의 자리만 유지 (BaseLayout 이 LNB 바로 앞에 렌더링) */
|
||||
showSidebarTrigger?: boolean;
|
||||
}) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [logoWidth, setLogoWidth] = useState(0);
|
||||
@@ -276,7 +279,11 @@ export function Header({
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-2 sm:w-auto sm:justify-start sm:px-4 md:px-6">
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
|
||||
{showSidebarTrigger ? (
|
||||
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
|
||||
) : (
|
||||
<span aria-hidden="true" className="size-12 shrink-0" />
|
||||
)}
|
||||
<h1>
|
||||
{/* 로고·사이트명 클릭 시 메인으로 이동 */}
|
||||
<a
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { PopupProvider } from "../../provider/popup-provider";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { SidebarProvider, SidebarTrigger } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import { Footer } from "../footer";
|
||||
@@ -17,6 +17,7 @@ export function BaseLayout({
|
||||
tokenManager,
|
||||
currentPath,
|
||||
handleMenuClick,
|
||||
sidebarTriggerPlacement = "header",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
@@ -25,7 +26,15 @@ export function BaseLayout({
|
||||
tokenManager?: TokenManager;
|
||||
currentPath?: string;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
/**
|
||||
* LNB 토글 버튼의 DOM 위치.
|
||||
* - "header": 헤더 로고(h1) 앞 (기존)
|
||||
* - "lnb": LNB 바로 앞 (콘텐츠 선형 구조 준수). 화면 위치는 CSS 로 헤더 좌측에 동일하게 유지
|
||||
*/
|
||||
sidebarTriggerPlacement?: "header" | "lnb";
|
||||
}) {
|
||||
const isTriggerBeforeLnb = sidebarTriggerPlacement === "lnb";
|
||||
|
||||
return (
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<PopupProvider>
|
||||
@@ -35,7 +44,15 @@ export function BaseLayout({
|
||||
<Header
|
||||
authService={authService}
|
||||
tokenManager={authService ? undefined : tokenManager}
|
||||
showSidebarTrigger={!isTriggerBeforeLnb}
|
||||
/>
|
||||
{/* 접기/펼치기 버튼을 제어 대상(LNB) 바로 앞에 배치하고, 화면에서는 헤더 좌측(기존 위치)에 고정 */}
|
||||
{isTriggerBeforeLnb && (
|
||||
<SidebarTrigger
|
||||
aria-controls="lnb"
|
||||
className="fixed left-2 top-0 z-40 size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground sm:left-4 sm:top-[1.5rem] md:left-6"
|
||||
/>
|
||||
)}
|
||||
<div className="flex">
|
||||
<LNB
|
||||
currentPath={currentPath}
|
||||
|
||||
@@ -271,15 +271,38 @@ export function LNB({
|
||||
);
|
||||
}
|
||||
|
||||
// 1뎁스 단일 메뉴: 하위 메뉴가 있는 메뉴(MenuGroup)와 동일한 마크업·간격·색상으로 표시
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={menuClassName}
|
||||
className={cn(menuClassName, "flex items-center pr-2")}
|
||||
data-matched={isMatched}
|
||||
tooltip={item.menuKornNm}
|
||||
>
|
||||
<a {...linkProps}>{menuContent}</a>
|
||||
<a {...linkProps}>
|
||||
<div className="flex items-center gap-2">
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon",
|
||||
`di-${item.iconNm}`,
|
||||
isMatched ? "text-primary" : "text-label",
|
||||
!hideLabel && "ml-2",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
isMatched
|
||||
? "text-primary"
|
||||
: "text-label group-hover/menu-button:text-foreground",
|
||||
hideLabel && "hidden",
|
||||
)}
|
||||
>
|
||||
{item.menuKornNm}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,8 +9,26 @@ import {
|
||||
DialogTrigger,
|
||||
} from "../dialog";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getMenuDetail } from "../../api";
|
||||
import { getMenuDetail, getSiteInfo } from "../../api";
|
||||
import { ENDPOINT } from "../../config";
|
||||
import { getCookie } from "../../lib";
|
||||
|
||||
/** 공통 레이아웃 밖 페이지(대시보드·로그인·오류 등)의 문서 제목. 각 앱 layout metadata 의 title 과 동일하게 유지 */
|
||||
const PLATFORM_TITLE = "유역하수도통합플랫폼 Hasuon";
|
||||
|
||||
const DASHBOARD_MENU_NAME = "대시보드";
|
||||
|
||||
/** 문서 제목: "유역하수도통합플랫폼 Hasuon - 사이트명 > 메뉴1 > 메뉴2" (대시보드는 플랫폼 제목만) */
|
||||
function buildDocumentTitle(siteNm: string | undefined, title: string, menuPaths: string[]) {
|
||||
const menus = (menuPaths.length > 0 ? menuPaths : [title]).filter(Boolean);
|
||||
const isDashboard =
|
||||
menus[menus.length - 1] === DASHBOARD_MENU_NAME ||
|
||||
(typeof window !== "undefined" && /\/dashboard\/?$/.test(window.location.pathname));
|
||||
|
||||
if (menus.length === 0 || isDashboard) return PLATFORM_TITLE;
|
||||
|
||||
return `${PLATFORM_TITLE} - ${[siteNm, ...menus].filter(Boolean).join(" > ")}`;
|
||||
}
|
||||
|
||||
const PageTitle = ({
|
||||
title,
|
||||
@@ -29,6 +47,24 @@ const PageTitle = ({
|
||||
enabled: !!menuCd,
|
||||
});
|
||||
|
||||
// 헤더와 같은 쿼리 키를 사용해 캐시된 사이트 정보를 재사용
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
const { data: siteInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
|
||||
queryFn: () => getSiteInfo(siteId),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
const documentTitle = buildDocumentTitle(siteInfo?.siteNm, title, menuPaths);
|
||||
|
||||
// 메뉴 이동 시마다 문서 제목 갱신, 레이아웃을 벗어나면 플랫폼 제목으로 복원
|
||||
useEffect(() => {
|
||||
document.title = documentTitle;
|
||||
return () => {
|
||||
document.title = PLATFORM_TITLE;
|
||||
};
|
||||
}, [documentTitle]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
Reference in New Issue
Block a user