diff --git a/apps/dm/app/(with-layout)/contents/page.tsx b/apps/dm/app/(with-layout)/contents/page.tsx
new file mode 100644
index 000000000..27d0974be
--- /dev/null
+++ b/apps/dm/app/(with-layout)/contents/page.tsx
@@ -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 ;
+}
diff --git a/apps/dm/app/(with-layout)/example/page.tsx b/apps/dm/app/(with-layout)/example/page.tsx
index b32b3346e..94e908d81 100644
--- a/apps/dm/app/(with-layout)/example/page.tsx
+++ b/apps/dm/app/(with-layout)/example/page.tsx
@@ -1,3 +1,6 @@
+/**
+ * 물 재이용 사례
+ */
import SubPageHeader from "@/components/SubPageHeader";
import BusinessSubNav, { type BusinessSubNavMenu } from "@/components/BusinessSubNav";
diff --git a/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx b/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
index cd660a929..f7eb49a84 100644
--- a/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
+++ b/apps/dm/app/(with-layout)/status/_components/SewageStatusSection.tsx
@@ -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 (
{isSubItem ? (
diff --git a/apps/dm/app/(with-layout)/status/page.tsx b/apps/dm/app/(with-layout)/status/page.tsx
index b18662c06..8bed3d04f 100644
--- a/apps/dm/app/(with-layout)/status/page.tsx
+++ b/apps/dm/app/(with-layout)/status/page.tsx
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
id: "status-treated-usage",
- label: "연도별 하수처리수 재이용 현황",
+ label: "용도별 하수처리수 재이용 현황",
},
{
id: "status-treated-facility",
diff --git a/apps/dm/app/layout.tsx b/apps/dm/app/layout.tsx
index 1fd9ea1cc..49c6c357c 100644
--- a/apps/dm/app/layout.tsx
+++ b/apps/dm/app/layout.tsx
@@ -16,7 +16,7 @@ export default function RootLayout({
물재이용 대민
-
+
{children}
diff --git a/apps/dm/app/main/layout.tsx b/apps/dm/app/main/layout.tsx
index b1304a66a..873024784 100644
--- a/apps/dm/app/main/layout.tsx
+++ b/apps/dm/app/main/layout.tsx
@@ -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 (
-
{children} {/* TopNav 높이만큼 */}
);
diff --git a/apps/dm/app/store/store.tsx b/apps/dm/app/store/store.tsx
index c1d4fcdfd..3b7b2c24d 100644
--- a/apps/dm/app/store/store.tsx
+++ b/apps/dm/app/store/store.tsx
@@ -33,7 +33,7 @@ export const StoreZus = (
return createStore ()((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 {
diff --git a/apps/dm/components/BusinessSubNav.tsx b/apps/dm/components/BusinessSubNav.tsx
index 6b4246412..d8753b5dd 100644
--- a/apps/dm/components/BusinessSubNav.tsx
+++ b/apps/dm/components/BusinessSubNav.tsx
@@ -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 = {};
+ const nextOpenState: Record = {};
- menus.forEach((menu) => {
+ menus.forEach((menu) => {
nextOpenState[menu.key] = menu.key === parentMenu.key;
- });
+ });
- return nextOpenState;
+ return nextOpenState;
});
}, [activeId, menus]);
diff --git a/apps/dm/components/ui/contents/contents-api.ts b/apps/dm/components/ui/contents/contents-api.ts
new file mode 100644
index 000000000..3a7c64639
--- /dev/null
+++ b/apps/dm/components/ui/contents/contents-api.ts
@@ -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"],
+);
diff --git a/apps/dm/components/ui/contents/contentsPage.tsx b/apps/dm/components/ui/contents/contentsPage.tsx
new file mode 100644
index 000000000..c823acd42
--- /dev/null
+++ b/apps/dm/components/ui/contents/contentsPage.tsx
@@ -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 ;
+}
+
+export { ContentsPage };
diff --git a/apps/dm/components/ui/contents/contentsPageClient.tsx b/apps/dm/components/ui/contents/contentsPageClient.tsx
new file mode 100644
index 000000000..97c62bb83
--- /dev/null
+++ b/apps/dm/components/ui/contents/contentsPageClient.tsx
@@ -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(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 (
+
+ );
+}
+
+export { ContentsPageClient };
diff --git a/apps/dm/components/ui/contents/index.ts b/apps/dm/components/ui/contents/index.ts
new file mode 100644
index 000000000..82a5dad4f
--- /dev/null
+++ b/apps/dm/components/ui/contents/index.ts
@@ -0,0 +1 @@
+export { ContentsPage } from "./contentsPage";
diff --git a/apps/dm/middleware.ts b/apps/dm/middleware.ts
index f4f145217..c2a228931 100644
--- a/apps/dm/middleware.ts
+++ b/apps/dm/middleware.ts
@@ -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}` : ""}`;
diff --git a/apps/dm/next.config.ts b/apps/dm/next.config.ts
index 5b5bf85da..bbe67b0ce 100644
--- a/apps/dm/next.config.ts
+++ b/apps/dm/next.config.ts
@@ -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: [],
};
diff --git a/apps/dm/src/entities/content/api/contents-service.ts b/apps/dm/src/entities/content/api/contents-service.ts
new file mode 100644
index 000000000..292d9b795
--- /dev/null
+++ b/apps/dm/src/entities/content/api/contents-service.ts
@@ -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 {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getLatestApprovedContentsInfo(
+ contsSn: number,
+ menuCd: string,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
+ { params: { menuCd } },
+ );
+ return response.data;
+ }
+
+ public async getContentsVersionList(
+ params: ContentsVersionParams,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async getLatestContentsInfo(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ const response = await client.get(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ );
+ return response.data;
+ }
+
+ public async createContents(data: ContentsFormData): Promise {
+ const response = await client.post(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async updateContents(
+ contsSn: number,
+ contsVer: number,
+ data: ContentsFormData,
+ ): Promise {
+ const response = await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ data,
+ );
+ return response.data;
+ }
+
+ public async deleteContents(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.delete(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
+ );
+ }
+
+ public async requestApprove(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`,
+ );
+ }
+
+ public async approveContents(
+ contsSn: number,
+ contsVer: number,
+ ): Promise {
+ await client.put(
+ `${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
+ );
+ }
+}
+
+export const ContentsApi = new ContentsApiService();
diff --git a/apps/dm/src/entities/content/index.ts b/apps/dm/src/entities/content/index.ts
new file mode 100644
index 000000000..2446dd716
--- /dev/null
+++ b/apps/dm/src/entities/content/index.ts
@@ -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";
diff --git a/apps/dm/src/entities/content/model/api-interface.ts b/apps/dm/src/entities/content/model/api-interface.ts
new file mode 100644
index 000000000..8218bfb0d
--- /dev/null
+++ b/apps/dm/src/entities/content/model/api-interface.ts
@@ -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 {
+ 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;
+}
diff --git a/apps/dm/src/entities/content/model/contents-interface.ts b/apps/dm/src/entities/content/model/contents-interface.ts
new file mode 100644
index 000000000..474dfe97c
--- /dev/null
+++ b/apps/dm/src/entities/content/model/contents-interface.ts
@@ -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;
+export type ContentsDetail = z.infer;
+export type ContentsFormData = z.infer;
+export type ContentsList = PageableList;
+export type ContentsVersionList = PageableList;
+
+export interface ContentsService {
+ getContentsList(params: ContentsParams): Promise;
+ getLatestApprovedContentsInfo(
+ contsSn: number,
+ menuCd: string,
+ ): Promise;
+ getContentsVersionList(
+ params: ContentsVersionParams,
+ ): Promise;
+ getLatestContentsInfo(
+ contsSn: number,
+ contsVer: number,
+ ): Promise;
+ createContents(data: ContentsFormData): Promise;
+ updateContents(
+ contsSn: number,
+ contsVer: number,
+ data: ContentsFormData,
+ ): Promise;
+ deleteContents(contsSn: number, contsVer: number): Promise;
+ requestApprove(contsSn: number, contsVer: number): Promise;
+ approveContents(contsSn: number, contsVer: number): Promise;
+}
|