Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into origin/iipbs/st/dev
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
id: "status-treated-usage",
|
||||
label: "연도별 하수처리수 재이용 현황",
|
||||
label: "용도별 하수처리수 재이용 현황",
|
||||
},
|
||||
{
|
||||
id: "status-treated-facility",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentsPage } from "./contentsPage";
|
||||
@@ -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}` : ""}`;
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user