Compare commits

..
Author SHA1 Message Date
changwng 182af7cf09 222 2025-04-01 17:05:28 +09:00
changwng 84caaa3977 p_atflCd. 추가로직 정리 2025-04-01 17:04:47 +09:00
1894 changed files with 15671 additions and 125296 deletions
-9
View File
@@ -1,9 +0,0 @@
node_modules
.next
Dockerfile
.dockerignore
.env*
apps/**/node_modules
packages/**/node_modules
apps/**/.next
**/.next/cache
+1 -3
View File
@@ -14,9 +14,7 @@ dist
# misc
.DS_Store
apps/wr/.next/**/*
apps/fo/.next/**/*
# env
#.env*
.env*
.qodo
-93
View File
@@ -1,93 +0,0 @@
# ---------------------- #
# STEP 1: Base 설치 환경 #
# ---------------------- #
FROM node:20-alpine AS base
WORKDIR /repo
# pnpm 설치 (corepack)
RUN corepack enable && corepack prepare pnpm@10.12.1 --activate
# 필요한 시스템 패키지 설치 (선택)
RUN apk add --no-cache libc6-compat
# ---------------------- #
# STEP 2: 의존성 설치용 캐시 레이어 #
# ---------------------- #
FROM base AS deps
# workspace 정의 복사
COPY pnpm-workspace.yaml ./
COPY package.json pnpm-lock.yaml ./
COPY apps/cs/package.json ./apps/cs/
COPY packages ./packages
# 종속성 설치 (only production dependencies)
RUN rm -rf node_modules .next
RUN pnpm install --frozen-lockfile --node-linker=node-modules
#RUN pnpm install --frozen-lockfile --node-linker=node-modules
RUN ls -al .
#RUN pnpm install --prod
# ---------------------- #
# STEP 3: 빌드 단계 #
# ---------------------- #
FROM base AS builder
COPY --from=deps /repo /repo
WORKDIR /repo
# 전체 소스 복사
COPY . .
RUN ls -al /repo/apps/cs/.next || true
RUN rm -rf /repo/apps/cs/.next
RUN ls -al /repo/apps/cs/
# Next.js 빌드
RUN cat /repo/apps/cs/next.config.ts || true
RUN pnpm --filter ./apps/cs... build
#RUN ls -al .next/standalone
# 👉 실행에 필요한 모듈만 유지
#RUN pnpm prune --prod
# Next.js 빌드 내용 보기
RUN ls -al ./apps/cs/
RUN ls -al /repo/apps/cs/.next
RUN ls -al /repo/apps/cs/.next/chunks || true
# web browser request zip format
#RUN find /repo/apps/cs/.next/static -type f -name '*.js' -exec gzip -k {} \;
#RUN find ./apps/cs/.next -type f
# ---------------------- #
# STEP 4: 최종 러너 단계 (최소 런타임) #
# ---------------------- #
FROM node:20-alpine AS runner
WORKDIR /app
# corepack 사용 설정
#RUN corepack enable && corepack prepare pnpm@8.15.5 --activate
# 환경변수 설정
ENV NODE_ENV=production
ENV PORT=3000
# pnpm은 런타임에 필요 없음 → corepack 설치 생략 가능
# 빌드 결과만 복사
RUN rm -rf .next
COPY --from=builder /repo/apps/cs/.next/standalone ./
COPY --from=builder /repo/apps/cs/public ./public
COPY --from=builder /repo/apps/cs/.next/static ./apps/cs/.next/static
COPY --from=builder /repo/apps/cs/.env ./apps/cs/.env
COPY --from=builder /repo/apps/cs/package.json ./apps/cs/package.json
COPY --from=builder /repo/apps/cs/next.config.ts ./apps/cs/next.config.ts
# 헬스체크 등 필요 시 추가 가능
#HEALTHCHECK --interval=30s --timeout=3s CMD wget --spider -q http://localhost:3000 || exit 1
EXPOSE 3000
CMD ["node", "apps/cs/server.js"]
#CMD ["pnpm", "start"]
+45 -62
View File
@@ -44,12 +44,6 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
```json
{
// 기타 설정 ...
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/components": "workspace:*", // 추가
"react": "^19.0.0", // 제거
@@ -67,10 +61,10 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
```json
{
// 기타 설정 ...
"baseUrl": "./src",
"paths": {
"@common/components": ["../../../packages/common/components/dist/"],
"@common/config": ["../../../packages/common/config/dist/"],
"@common/components/*": ["../../../packages/common/components/src/*"],
"@common/components": ["../../../packages/common/components/src"],
"@common/network/*": ["../../../packages/common/network/*"],
"@common/network": ["../../../packages/common/network/"]
}
}
@@ -95,47 +89,61 @@ export default {
### 5. next.config.ts 수정
```json
import type { NextConfig } from "next";
import webpack from "webpack";
const subDomain =
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
const nextConfig: NextConfig = {
webpack: (config) => {
config.plugins.push(
// common/components의 vite 환경 변수를 next 환경 변수로 정의
new webpack.DefinePlugin({
"import.meta.env.VITE_PORT": JSON.stringify(
process.env.NEXT_PUBLIC_PORT,
),
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
process.env.NEXT_PUBLIC_PROXY_HOST,
),
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
process.env.NEXT_PUBLIC_SERVER_API_URL,
),
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
process.env.NEXT_PUBLIC_CLAIM_NAME,
),
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
),
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
),
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
process.env.NEXT_PUBLIC_AUTH_USER_ID,
),
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
}),
);
return config;
},
async rewrites() {
return {
beforeFiles: [
{
source: "/_next/static/:path*",
destination: `${subDomain}/_next/static/:path*`,
has: [
{
type: "header",
key: "referer",
value: "(.*)/cms(.*)",
},
],
},
],
afterFiles: [
{
source: "/cms/:path*",
destination: `${subDomain}/cms/:path*`,
},
],
fallback: [],
};
return [
{
source: "/server/:path*",
destination: `${serverApiUrl}/:path*`,
},
];
},
};
export default nextConfig;
```
### 6. globals.css(또는 전역 css) 수정
```css
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@import "@common/components/index.css";
@import "@common/assets/index.css";
@tailwind base;
@tailwind components;
@@ -151,26 +159,6 @@ rm -rf node_modules pnpm-lock.yaml
pnpm install
```
### 8. 로그아웃 처리 가이드
* 참고 파일
- apps/cs/app/api/auth/logout/route.ts
- packages/common/components/src/ui/header/index.tsx (55-65 line)
각 서비스 폴더에 ‘/app/api/auth/logout/route.ts’ 생성해야 합니다(코드는 cs에서 확인).
** 로그아웃 처리되면 ‘/auth/login’ 페이지로 이동됩니다.
### 9. 'SITE_ID’ 환경변수를 쿠키로 대체하는 가이드(공통 모듈 사용하기위한 필수 사항)
* 참고 파일: apps/cs/src/shared/api/client.ts (쿠키 저장 처리)
apps/cs/src/shared/lib/cookie.ts (쿠키 공통 모듈)
`SITE_ID` 환경변수를 처음 사용하는 파일 또는 최상단 클라이언트 컴포넌트에서 “site-id-key” 쿠키에 `SITE_ID` 저장
## 기술 스택
- React(^18)
@@ -196,8 +184,3 @@ http://dev.onasys.co.kr:8000
## Swagger
http://dev.onasys.co.kr:8000/webjars/swagger-ui/index.html?urls.primaryName=attach-service
## 개발 가이드
- 사이드바 메뉴 클릭 시 "menuCd" 데이터 쿠키 저장 처리
- 쿠키명 : menu-code-key
- 해당 메뉴 쿠키 데이터 가져와 페이지 권한 및 상당 Breadcrumb 데이터 전달 처리
-40
View File
@@ -1,40 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env*
-3
View File
@@ -1,3 +0,0 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
@@ -1,10 +0,0 @@
import { ContentsPage as VContentsPage } from "@/pages/contents";
export default async function ContentsPage({
params,
}: {
params: Promise<{ contsSn: string }>;
}) {
const { contsSn } = await params;
return <VContentsPage contsSn={parseInt(contsSn)} />;
}
@@ -1,28 +0,0 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { MenuApi } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { PageLayout } from "@/shared/ui/page-layout";
export default function ContentsLayout({
children,
}: {
children: React.ReactNode;
}) {
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, currentMenuInfo?.menuCd],
queryFn: () =>
MenuApi.getMenuInfo(currentMenuInfo?.menuCd.toString() ?? ""),
enabled: !!currentMenuInfo,
});
const menuPaths = data?.menuPath?.split(">").map((path) => path.trim());
return (
<PageLayout title={data?.menuKornNm ?? ""} menuPaths={menuPaths ?? []}>
{children}
</PageLayout>
);
}
@@ -1,10 +0,0 @@
import { ModifyPage } from "@/pages/(post)/faq";
export default async function PstBbsFaqPostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { FaqPage } from "@/pages/(post)/faq";
export default async function PstBbsFaqPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <FaqPage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/faq";
export default async function PstBbsFaqRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { ModifyPage } from "@/pages/(post)/image";
export default async function PstBbsImagePostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { ImagePost } from "@/pages/(post)/image";
export default async function PstBbsImagePostPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ImagePost bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { ImagePage } from "@/pages/(post)/image";
export default async function PstBbsImagePage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <ImagePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/image";
export default async function PstBbsImageRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { ModifyPage } from "@/pages/(post)/movie";
export default async function PstBbsMoviePostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { MoviePost } from "@/pages/(post)/movie";
export default async function PstBbsMoviePostPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <MoviePost bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { MoviePage } from "@/pages/(post)/movie";
export default async function PstBbsMoviePage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <MoviePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/movie";
export default async function PstBbsMovieRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/normal";
export default async function PstBbsNormalAnswerPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <CreatePage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { ModifyPage } from "@/pages/(post)/normal";
export default async function PstBbsNormalPostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { NormalPost } from "@/pages/(post)/normal";
export default async function PstBbsNormalPostPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <NormalPost bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { NormalPage } from "@/pages/(post)/normal";
export default async function PstBbsNormalPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <NormalPage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/normal";
export default async function PstBbsNormalRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { ModifyPage } from "@/pages/(post)/notice";
export default async function PstBbsNoticePostModifyPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { NoticePost } from "@/pages/(post)/notice";
export default async function PstBbsNoticePostPage({
params,
}: {
params: Promise<{ bbsId: string; pstNo: string }>;
}) {
const { bbsId, pstNo } = await params;
return <NoticePost bbsId={bbsId} pstNo={pstNo} />;
}
@@ -1,10 +0,0 @@
import { NoticePage } from "@/pages/(post)/notice";
export default async function PstBbsNoticePage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <NoticePage bbsId={bbsId} />;
}
@@ -1,10 +0,0 @@
import { CreatePage } from "@/pages/(post)/notice";
export default async function PstBbsNoticeRegisterPage({
params,
}: {
params: Promise<{ bbsId: string }>;
}) {
const { bbsId } = await params;
return <CreatePage bbsId={bbsId} />;
}
@@ -1,28 +0,0 @@
"use client";
import { MenuApi } from "@/entities/menu";
import { ENDPOINT } from "@/shared/config";
import { useMenuStore } from "@/shared/lib";
import { PageLayout } from "@/shared/ui/page-layout";
import { useQuery } from "@tanstack/react-query";
export default function PostLayout({
children,
}: {
children: React.ReactNode;
}) {
const { bbsInfo, currentMenuInfo } = useMenuStore((state) => state);
const { data } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, currentMenuInfo?.menuCd],
queryFn: () =>
MenuApi.getMenuInfo(currentMenuInfo?.menuCd.toString() ?? ""),
enabled: !!currentMenuInfo,
});
const menuPaths = data?.menuPath?.split(">").map((path) => path.trim());
return (
<PageLayout title={bbsInfo?.bbsNm ?? ""} menuPaths={menuPaths ?? []}>
{children}
</PageLayout>
);
}
@@ -1,34 +0,0 @@
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { BaseLayout } from "@common/components";
import type { MenuRole } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
export default function WithBaseLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const queryClient = useQueryClient();
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
const handleMenuClick = (item: MenuRole) => {
setCurrentMenuInfo(item);
router.push(item.urlPath);
};
return (
<BaseLayout
queryClient={queryClient}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
{children}
</BaseLayout>
);
}
@@ -1,2 +0,0 @@
import { SkeletonPage } from "@/shared/ui/skeleton-page";
export default SkeletonPage;
@@ -1,17 +0,0 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
REFRESH_TOKEN,
} from "@/shared/config";
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete(ACCESS_TOKEN);
cookieStore.delete(REFRESH_TOKEN);
cookieStore.delete(AUTH_USER_ID);
cookieStore.delete(CLAIM_NAME);
return NextResponse.json({ message: "Logged out" }, { status: 200 });
}
@@ -1,100 +0,0 @@
import {
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
REFRESH_TOKEN,
SERVER_API_URL,
} from "@/shared/config";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
async function handler(
request: Request,
params: { url: string[] },
method: "GET" | "POST" | "PUT" | "DELETE",
) {
const cookieStore = await cookies();
const siteId = cookieStore.get("site-id-key")?.value ?? "";
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Site-Id": siteId,
};
let body = {};
const isRefreshHandler = params.url.includes("refresh");
// silent refresh
if (isRefreshHandler) {
const refreshToken = cookieStore.get(REFRESH_TOKEN);
headers[CLAIM_NAME] = refreshToken?.value ?? "";
if (!headers[CLAIM_NAME]) {
return NextResponse.json({ message: "Can't refresh" }, { status: 401 });
}
}
try {
if (!isRefreshHandler) {
body = await request.json();
}
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
method,
headers,
body: JSON.stringify(body),
});
if (!result.ok) {
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
}
const accessToken = result.headers.get(ACCESS_TOKEN);
const refreshToken = result.headers.get(REFRESH_TOKEN);
const userId = result.headers.get(AUTH_USER_ID);
if (accessToken && userId) {
const response = NextResponse.json({
[ACCESS_TOKEN]: accessToken,
[AUTH_USER_ID]: userId,
});
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "lax",
path: "/",
});
}
return response;
}
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
);
} catch (error) {
return NextResponse.json(
{ message: "Server Error", error },
{ status: 500 },
);
}
}
export async function POST(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
return handler(request, params, "POST");
}
export async function PUT(
request: Request,
props: { params: Promise<{ url: string[] }> },
) {
const params = await props.params;
return handler(request, params, "PUT");
}
@@ -1,2 +0,0 @@
import { LoginPage } from "@/pages/auth/login";
export default LoginPage;
-8
View File
@@ -1,8 +0,0 @@
import type { Metadata } from "next";
import { App } from "@/app";
export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼",
};
export default App;
-21
View File
@@ -1,21 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"react-hooks/exhaustive-deps": "off",
},
},
];
export default eslintConfig;
-42
View File
@@ -1,42 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
const serverApiUrl =
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:13000";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
if (pathname.startsWith("/server")) {
const queryParams = searchParams.toString();
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
queryParams ? `?${queryParams}` : ""
}`;
console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
console.log(
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
return NextResponse.next();
}
export const config = {
matcher: ["/server/:path*", "/attach-service/:path*"],
};
-16
View File
@@ -1,16 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async rewrites() {
return {
afterFiles: [
{
source: "/cms/:path*",
destination: "/:path*",
},
],
};
},
};
export default nextConfig;
-56
View File
@@ -1,56 +0,0 @@
{
"name": "features",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 13000",
"build": "next build",
"start": "next start -p 13000",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "^3.5.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^6.1.0",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"next": "15.3.0",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"sonner": "^2.0.1",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
View File
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

-20
View File
@@ -1,20 +0,0 @@
import "./styles/base.css";
import "./styles/components.css";
import "@fontsource-variable/noto-sans-kr";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
const theme = (await cookies()).get("site-theme-key");
return (
<html lang="ko" data-theme={theme?.value}>
<body>
<AppProvider>{children}</AppProvider>
</body>
</html>
);
};
export { App };
@@ -1,38 +0,0 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
import { Toaster } from "@/shared/ui/sonner";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { PromptDialog } from "@/shared/ui/prompt-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { StoreProvider, PromptProvider } from "@/shared/providers";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<PromptProvider>
<ConfirmProvider>
<AuthWrapper>
<MenuWrapper>{children}</MenuWrapper>
<AlertDialog />
<PromptDialog />
<ConfirmDialog />
<Toaster position="top-center" />
</AuthWrapper>
</ConfirmProvider>
</PromptProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
};
export { AppProvider };
@@ -1 +0,0 @@
export { AppProvider } from "./app-provider";
@@ -1,20 +0,0 @@
@import "@common/assets/styles/base.css";
@import "@common/assets/styles/diveicon.css";
@import "@common/components/index.css";
@tailwind base;
@tailwind utilities;
@layer base {
:root {
--secondary-background: rgba(0, 0, 0, 0.04);
}
[data-theme="black"] {
--secondary-background: rgba(255, 255, 255, 0.09);
}
html,
body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
}
}
@@ -1,129 +0,0 @@
.card {
position: relative;
margin: 0.6rem;
box-sizing: border-box;
min-height: 2rem;
border-radius: 0.5rem;
padding: 0.6rem;
font-size: 1.3rem;
color: #666;
}
.card_width10 {
width: calc(10% - 12px);
}
.card_width20 {
width: calc(20% - 12px);
}
.card_width25 {
width: calc(25% - 12px);
}
.card_width30 {
width: calc(30% - 12px);
}
.card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {
width: calc(40% - 12px);
}
.card_width45 {
width: calc(45% - 12px);
}
.card_width50 {
width: calc(50% - 12px);
}
.card_width60 {
width: calc(60% - 12px);
}
.card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {
width: calc(70% - 12px);
}
.card_width75 {
width: calc(75% - 12px);
}
.card_width80 {
width: calc(80% - 12px);
}
.card_width90 {
width: calc(90% - 12px);
}
.card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.topline {
border-top: 1px solid #ccc;
}
.bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border {
border: 1px solid #dbe2e6;
background-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e1e7eb;
padding: 1rem;
}
.card-footer {
margin-left: -0.6rem;
margin-right: -0.6rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e7eb;
padding-top: 1rem;
padding-bottom: 1rem;
}
/* 블랙 테마 */
[data-theme="black"] {
.card-border {
background-color: rgba(255, 255, 255, 0.065);
border-color: #38383a;
}
.card-header {
border-bottom-color: #3f4042;
}
}
@@ -1,90 +0,0 @@
import type { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
Attachment,
AttachmentList,
AttachmentParams,
AttachmentService,
} from "../model/attachment-interface";
export class AttachmentApiService implements AttachmentService {
public async getAttachmentsList(
params: AttachmentParams,
): Promise<AttachmentList> {
const response = await client.get<AttachmentList>(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}`,
{ params },
);
return response.data;
}
public async getAttachmentsInfo(atflCd: string): Promise<Attachment[]> {
const response = await client.get<Attachment[]>(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`,
);
return response.data;
}
public async deleteAttachmentByAtflId(atflId: string): Promise<void> {
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
}
public async deleteAttachmentByAtflCd(atflCd: string): Promise<void> {
await client.delete(
`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`,
);
}
public async getImageUrlByAtflId(atflId: string): Promise<string> {
if (!atflId) return "";
const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getImageUrlByAtflCd(atflCd: string): Promise<string> {
if (!atflCd) return "";
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getStreamVideoUrlByAtflId(atflId: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO}/${atflId}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getMovieThumbnailUrlByAtflId(atflId: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/${atflId}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async getMovieThumbnailUrlByAtflCd(atflCd: string): Promise<string> {
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/atflCd/${atflCd}`,
);
const blob = await response.blob();
return URL.createObjectURL(blob);
}
public async downloadAttachmentByAtflCd(
atflCd: string,
): Promise<AxiosResponse> {
const response = await client.get(
`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`,
{ responseType: "blob" },
);
return response;
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -1,5 +0,0 @@
export { AttachmentApi } from "./api/attachment-service";
export type {
AttachmentParams,
Attachment,
} from "./model/attachment-interface";
@@ -1,47 +0,0 @@
import type { AxiosResponse } from "axios";
import type { PageableList, PageableParams } from "@/shared/model";
export interface AttachmentParams extends PageableParams {
keywordType?: string;
keyword?: string;
atflCd?: string;
atflSn?: number;
atflId?: string;
orgnlFileNm?: string;
atflFlieSz?: number;
atflType?: string;
dwnldNocs?: number;
delYn?: string;
lnkgDmnId?: string;
lnkgDmnNm?: string;
}
export interface Attachment {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export type AttachmentList = PageableList<Attachment>;
export interface AttachmentService {
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
getImageUrlByAtflId: (atflId: string) => Promise<string>;
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
getStreamVideoUrlByAtflId: (atflId: string) => Promise<string>;
getMovieThumbnailUrlByAtflId: (atflId: string) => Promise<string>;
getMovieThumbnailUrlByAtflCd: (atflCd: string) => Promise<string>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
}
@@ -1,85 +0,0 @@
import { AxiosError } from "axios";
import {
ENDPOINT,
JWT_EXPIRATION_TIME,
ACCESS_TOKEN,
AUTH_USER_ID,
CLAIM_NAME,
} from "@/shared/config";
import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
export class LoginApiService implements LoginService {
private static instance: LoginApiService;
private constructor() {}
public static getInstance(): LoginApiService {
if (!LoginApiService.instance) {
LoginApiService.instance = new LoginApiService();
}
return LoginApiService.instance;
}
public async login(payload: ILogin): Promise<void> {
try {
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
method: "POST",
body: JSON.stringify({
userId: payload.userId,
password: payload.password,
}),
});
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setupRefreshTimer(
() => this.silentRefresh(),
JWT_EXPIRATION_TIME,
);
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(`Login failed: ${error.response?.data?.message}`);
}
throw new Error(`Login failed: ${error}`);
}
}
public async silentRefresh(): Promise<void> {
try {
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{ method: "PUT" },
);
const data = await response.json();
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
} catch (error) {
tokenManager.clearAuthToken();
if (error instanceof AxiosError) {
throw new Error(
`Silent refresh failed: ${error.response?.data?.message}`,
);
}
throw new Error(`Silent refresh failed: ${error}`);
}
}
}
export const LoginApi = LoginApiService.getInstance();
@@ -1,2 +0,0 @@
export { LoginApi } from "./api/login-service";
export { AuthWrapper } from "./lib/auth-wrapper";
@@ -1,40 +0,0 @@
"use client";
import { useEffect } from "react";
import { client } from "@/shared/api";
import type { IUserInfo } from "@/shared/store";
import { getCookie, useUserStore } from "@/shared/lib";
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
import { LoginApi } from "../api/login-service";
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
throw new Error("Missing required authentication tokens");
}
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
if (!userInfo) setUserInfo(response.data);
}
useEffect(() => {
(async () => {
try {
await authCheck();
} catch (error) {
console.error(error);
await LoginApi.silentRefresh();
await authCheck();
}
})();
}, []);
return <>{children}</>;
};
export { AuthWrapper };
@@ -1,59 +0,0 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
private constructor() {}
public static getInstance(): TokenManager {
if (!TokenManager.instance) {
TokenManager.instance = new TokenManager();
}
return TokenManager.instance;
}
public getToken(key: string): string | undefined {
return getCookie(key);
}
public setToken(key: string, value: string): void {
setCookie(key, value, {
secure: window.location.protocol === "https:",
sameSite: "lax",
});
}
public removeToken(key: string): void {
removeCookie(key);
}
public setupRefreshTimer(
callback: () => Promise<void>,
expirationTime: number,
): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
}
this.refreshTimer = setTimeout(() => {
callback().catch((error) => {
console.error("Silent refresh failed:", error);
this.clearAuthToken();
});
}, expirationTime - 60000);
}
public clearAuthToken(): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
this.removeToken(CLAIM_NAME);
this.removeToken(AUTH_USER_ID);
}
}
export const tokenManager = TokenManager.getInstance();
@@ -1,9 +0,0 @@
export interface ILogin {
userId: string;
password: string;
}
export interface LoginService {
login(payload: ILogin): Promise<void>;
silentRefresh(): Promise<void>;
}
@@ -1,60 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
Board,
BoardFormData,
BoardList,
BoardParams,
BoardPopupList,
BoardPopupParams,
BoardService,
} from "../model/board-interface";
export class BoardApiService implements BoardService {
public async getBoardList(params: BoardParams): Promise<BoardList> {
const response = await client.get<BoardList>(
`${ENDPOINT.BOARD_SERVICE.BBS}`,
{ params },
);
return response.data;
}
public async getBoardPopupList(
params: BoardPopupParams,
): Promise<BoardPopupList> {
const response = await client.get<BoardPopupList>(
`${ENDPOINT.BOARD_SERVICE.BBS}`,
{ params },
);
return response.data;
}
public async getBoardInfo(bbsId: string): Promise<Board> {
const response = await client.get<Board>(
`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`,
);
return response.data;
}
public async createBoard(data: BoardFormData): Promise<Board> {
const response = await client.post<Board>(
`${ENDPOINT.BOARD_SERVICE.BBS}`,
data,
);
return response.data;
}
public async updateBoard(bbsId: string, data: BoardFormData): Promise<Board> {
const response = await client.put<Board>(
`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`,
data,
);
return response.data;
}
public async deleteBoard(bbsId: string): Promise<void> {
await client.delete(`${ENDPOINT.BOARD_SERVICE.BBS}/${bbsId}`);
}
}
export const BoardApi = new BoardApiService();
@@ -1,10 +0,0 @@
export { BoardApi } from "./api/board-service";
export {
boardFormSchema,
BOARD_POSTING_CODE,
type Board,
type BoardFormData,
type BoardParams,
type BoardPopup,
type BoardPostingCode,
} from "./model/board-interface";
@@ -1,97 +0,0 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
import type { BbsType } from "@/shared/store/menu-store";
export const BOARD_POSTING_CODE = {
팝업게시: "01",
상단게시: "02",
기간게시: "03",
} as const;
export type BoardPostingCode =
(typeof BOARD_POSTING_CODE)[keyof typeof BOARD_POSTING_CODE];
export interface BoardParams extends PageableParams {
keywordType?: string;
keyword?: string;
bbsId?: number;
bbsTypeCd?: string;
bbsNm?: string;
bbsExpln?: string;
rfrncUseYn?: string;
atflUseYn?: string;
atflMaxCnt?: number;
atflMaxFileSz?: number;
ansUseYn?: string;
cmntUseYn?: string;
dgstfnEvlUseYn?: string;
smsUseYn?: string;
upndFixUseYn?: string;
pstgPrdUseYn?: string;
popupUseYn?: string;
delYn?: string;
}
export interface BoardPopupParams extends PageableParams {
bbsNm?: string;
}
export interface Board {
bbsId: number;
bbsTypeCd: BbsType;
bbsTypeNm: string;
bbsNm: string;
bbsExpln: string;
rfrncUseYn: string;
atflUseYn: string;
atflMaxCnt: number;
atflMaxFileSz: number;
ansUseYn: string;
ansUseAuthrt: string;
cmntUseYn: string;
dgstfnEvlUseYn: string;
smsUseYn: string;
upndFixUseYn: string;
pstgPrdUseYn: string;
popupUseYn: string;
delYn: string;
useYn: string;
mdfcnDt: string;
mdfrNm: string;
}
export interface BoardPopup extends Board {
pstCnt: number;
}
export const boardFormSchema = z.object({
bbsTypeCd: z.string(),
bbsNm: z.string(),
bbsExpln: z.string(),
rfrncUseYn: z.string(),
atflUseYn: z.string(),
atflMaxCnt: z.number().nullish(),
atflMaxFileSz: z.number().nullish(),
ansUseYn: z.string(),
ansUseAuthrt: z.string().nullish(),
cmntUseYn: z.string(),
dgstfnEvlUseYn: z.string(),
smsUseYn: z.string(),
upndFixUseYn: z.string(),
pstgPrdUseYn: z.string(),
popupUseYn: z.string(),
useYn: z.string(),
});
export type BoardList = PageableList<Board>;
export type BoardPopupList = PageableList<BoardPopup>;
export type BoardFormData = z.infer<typeof boardFormSchema>;
export interface BoardService {
getBoardList: (params: BoardParams) => Promise<BoardList>;
getBoardPopupList: (params: BoardPopupParams) => Promise<BoardPopupList>;
getBoardInfo: (bbsId: string) => Promise<Board>;
createBoard: (data: BoardFormData) => Promise<Board>;
updateBoard: (bbsId: string, data: BoardFormData) => Promise<Board>;
deleteBoard: (bbsId: string) => Promise<void>;
}
@@ -1 +0,0 @@
export { CodeSelect } from "../ui/code-select";
@@ -1,14 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { CodeService, ICode } from "../model/code-interface";
export class CodeApiService implements CodeService {
public async getCode(upCd: string): Promise<ICode[]> {
const response = await client.get<ICode[]>(
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
);
return response.data;
}
}
export const CodeApi = new CodeApiService();
@@ -1,3 +0,0 @@
export { CodeApi } from "./api/code-service";
export type { ICode } from "./model/code-interface";
export { CodeSelect } from "./ui/code-select";
@@ -1,10 +0,0 @@
export interface ICode {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
unit: string;
}
export interface CodeService {
getCode(upCd: string): Promise<ICode[]>;
}
@@ -1,48 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
placeholder?: string;
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelect({
upCd,
defaultValue,
placeholder = "선택",
...props
}: CodeSelectProps) {
const { data: code } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
queryFn: () => CodeApi.getCode(upCd),
enabled: !!upCd,
});
const defaultValueText =
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
return (
<Select {...props}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder={defaultValueText} />
</SelectTrigger>
<SelectContent>
{code?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelect };
@@ -1,60 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentService,
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "../model/comment-interface";
export class CommentApiService implements CommentService {
public async getComments(params: CommentParams): Promise<CommentList> {
const response = await client.get<CommentList>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
{ params },
);
return response.data;
}
public async createComment(data: CommentFormData): Promise<Comment> {
const validatedData = commentFormSchema.parse(data);
const response = await client.post<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
validatedData,
);
return response.data;
}
public async updateComment(
bbsId: number,
pstNo: number,
cmntNo: number,
data: CommentUpdateFormData,
): Promise<Comment> {
const validatedData = commentUpdateFormSchema.parse(data);
const response = await client.put<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
validatedData,
);
return response.data;
}
public async deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment> {
const response = await client.delete<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
{ params: { cmntPswd } },
);
return response.data;
}
}
export const CommentApi = new CommentApiService();
@@ -1,10 +0,0 @@
export { CommentApi } from "./api/comment-service";
export {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "./model/comment-interface";
@@ -1,55 +0,0 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface CommentParams extends PageableParams {
keywordType?: string;
keyword?: string;
pstNo: number;
delYn?: string;
}
export interface Comment {
cmntEvl: number;
pstNo: number;
cmntNo: number;
cmntCn: string;
wrtrNm: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
delYn: string;
}
export const commentFormSchema = z.object({
pstNo: z.number(),
cmntCn: z.string(),
wrtrNm: z.string().nullish(),
cmntPswd: z.string(),
cmntEvl: z.number().nullish(),
});
export const commentUpdateFormSchema = commentFormSchema.omit({
pstNo: true,
});
export type CommentList = PageableList<Comment>;
export type CommentFormData = z.infer<typeof commentFormSchema>;
export type CommentUpdateFormData = z.infer<typeof commentUpdateFormSchema>;
export interface CommentService {
getComments(params: CommentParams): Promise<CommentList>;
createComment(data: CommentFormData): Promise<Comment>;
updateComment(
bbsId: number,
pstNo: number,
cmntNo: number,
data: CommentUpdateFormData,
): Promise<Comment>;
deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment>;
}
@@ -1,101 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/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();
@@ -1,8 +0,0 @@
export { ContentsApi } from "./api/contents-service";
export {
CONTENTS_APPROVE_STATUS,
contentsFormSchema,
type Contents,
type ContentsParams,
type ContentsFormData,
} from "./model/contents-interface";
@@ -1,105 +0,0 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
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>;
}
@@ -1,19 +0,0 @@
import { client } from "@/shared/api";
import type {
EditorService,
FileInfo,
UploadFile,
} from "../model/editor-interface";
import { ENDPOINT } from "@/shared/config";
export class EditorApiService implements EditorService {
async uploadImage(data: UploadFile): Promise<FileInfo> {
const response = await client.post<FileInfo>(
ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD,
data,
);
return response.data;
}
}
export const EditorApi = new EditorApiService();
@@ -1 +0,0 @@
export { Editor } from "./ui/editor";
@@ -1,10 +0,0 @@
export const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result as string;
resolve(base64.split(",")[1] || "");
};
reader.readAsDataURL(blob);
});
};
@@ -1,16 +0,0 @@
import { blobToBase64 } from "./blob-to-base64";
export const generateFileInfo = async (blobInfo: {
blob: () => Blob;
filename: () => string;
}) => {
const blob = blobInfo.blob();
const base64 = await blobToBase64(blob);
return {
fieldName: `${blob.type.split("/")[1]}`,
fileType: blob.type,
fileBase64: base64,
originalName: blobInfo.filename(),
size: blob.size,
};
};
@@ -1,37 +0,0 @@
import { SERVER_API_URL } from "@/shared/config";
export const editorBaseConfig = {
apiKey: process.env.NEXT_PUBLIC_TINY_API_KEY,
init: {
language: "ko_KR",
height: 500,
plugins: [
"advlist",
"autolink",
"lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"codesample",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
"save",
],
toolbar: [
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
"link image",
],
images_upload_base_path: SERVER_API_URL,
file_picker_types: "image",
},
};
@@ -1,20 +0,0 @@
export interface UploadFile {
fieldName: string;
fileType: string;
fileBase64: string;
originalName: string;
size: number;
}
export interface FileInfo {
originalFileName: string;
message: string;
fileType: string;
size: number;
uploaded: number;
url: string;
}
export interface EditorService {
uploadImage(data: UploadFile): Promise<FileInfo>;
}
@@ -1,61 +0,0 @@
import { useRef } from "react";
import {
Editor as TinyEditor,
type Editor as TinyEditorInstance,
type IAllProps,
} from "@tinymce/tinymce-react";
import { editorBaseConfig as baseConfig } from "../model/editor-base-config";
import { generateFileInfo } from "../lib/generate-file-info";
import { EditorApi } from "../api/editor-service";
const Editor = ({ ...props }: IAllProps) => {
const editorRef = useRef<TinyEditorInstance | null>(null);
const imagesUploadHandler = (blobInfo: {
blob: () => Blob;
filename: () => string;
}) => {
return new Promise(async (resolve) => {
const fileInfo = await generateFileInfo(blobInfo);
const data = await EditorApi.uploadImage(fileInfo);
resolve(data.url);
});
};
const filePickerCallback = (
callback: (base64: string, meta: { title: string }) => void,
) => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result as string;
callback(base64, { title: file.name });
};
reader.readAsDataURL(file);
};
input.click();
};
return (
<TinyEditor
{...baseConfig}
onInit={(_: unknown, editor: TinyEditorInstance) => {
editorRef.current = editor;
}}
init={{
...baseConfig.init,
disabled: props.disabled,
images_upload_handler: imagesUploadHandler,
file_picker_callback: filePickerCallback,
}}
{...props}
/>
);
};
export { Editor };
@@ -1 +0,0 @@
export { MenuAuthorityApi } from "../api/menu-authority-service";
@@ -1,40 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
MenuAuthorityList,
MenuAuthorityParams,
MenuAuthorityPayload,
MenuAuthorityService,
PageMenuAuthority,
} from "../model/menu-authority-interface";
export class MenuAuthorityApiService implements MenuAuthorityService {
public async getMenuAuthorityList(
params: MenuAuthorityParams,
): Promise<MenuAuthorityList> {
const response = await client.get<MenuAuthorityList>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
{ params },
);
return response.data;
}
public async getPageMenuAuthority(
menuCd: number,
): Promise<PageMenuAuthority[]> {
const response = await client.get<PageMenuAuthority[]>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
);
return response.data;
}
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
data,
);
return response.data;
}
}
export const MenuAuthorityApi = new MenuAuthorityApiService();
@@ -1,4 +0,0 @@
export { MenuAuthorityApi } from "./api/menu-authority-service";
export { type PageMenuAuthority } from "./model/menu-authority-interface";
export { AuthorityButton } from "./ui/authority-button";
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
@@ -1,48 +0,0 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface MenuAuthorityParams extends PageableParams {
groupId?: string;
menuCd?: number;
useYn?: string;
}
export interface PageMenuAuthority {
mngAuthrtYn: string;
inqAuthrtYn: string;
regAuthrtYn: string;
mdfcnAuthrtYn: string;
delAuthrtYn: string;
}
export const menuAuthoritySchema = z.object({
groupId: z.string(),
groupNm: z.string(),
menuCd: z.number(),
mngAuthrtYn: z.string(),
inqAuthrtYn: z.string(),
regAuthrtYn: z.string(),
mdfcnAuthrtYn: z.string(),
delAuthrtYn: z.string(),
useYn: z.string(),
mode: z.string(),
});
export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({
groupNm: true,
});
export const menuAuthorityFormSchema = z.object({
menuAuthority: z.array(menuAuthorityPayloadSchema),
});
export type MenuAuthority = z.infer<typeof menuAuthoritySchema>;
export type MenuAuthorityList = PageableList<MenuAuthority>;
export type MenuAuthorityPayload = z.infer<typeof menuAuthorityPayloadSchema>;
export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
export interface MenuAuthorityService {
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
}
@@ -1,117 +0,0 @@
import type { ColumnDef } from "@tanstack/react-table";
import type { UseFormReturn } from "react-hook-form";
import { Checkbox } from "@common/components";
import type {
MenuAuthority,
MenuAuthorityFormData,
MenuAuthorityPayload,
} from "./menu-authority-interface";
export const createColumns = (
form: UseFormReturn<MenuAuthorityFormData>,
): ColumnDef<MenuAuthority>[] => {
const getCurrentRow = (i: number) => form.watch("menuAuthority")[i];
const handleCheckedChange =
(i: number, key: keyof MenuAuthorityPayload) => (checked: boolean) => {
if (getCurrentRow(i)?.mode === "R") {
form.setValue(`menuAuthority.${i}.mode`, "U");
}
form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N");
};
return [
{
accessorKey: "groupNm",
header: "그룹명",
},
{
accessorKey: "mngAuthrtYn",
header: "관리권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.mngAuthrtYn === "Y"}
onCheckedChange={(checked) => {
if (checked) {
handleCheckedChange(
row.index,
"regAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"mdfcnAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"delAuthrtYn",
)(checked as boolean);
handleCheckedChange(
row.index,
"inqAuthrtYn",
)(checked as boolean);
}
handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean);
}}
/>
);
},
},
{
accessorKey: "regAuthrtYn",
header: "등록권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.regAuthrtYn === "Y"}
onCheckedChange={(checked) => {
if (checked) {
handleCheckedChange(
row.index,
"inqAuthrtYn",
)(checked as boolean);
}
handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean);
}}
/>
);
},
},
{
accessorKey: "mdfcnAuthrtYn",
header: "수정권한 ",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.mdfcnAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "mdfcnAuthrtYn")}
/>
);
},
},
{
accessorKey: "delAuthrtYn",
header: "삭제권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.delAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "delAuthrtYn")}
/>
);
},
},
{
accessorKey: "inqAuthrtYn",
header: "조회권한",
cell: ({ row }) => {
return (
<Checkbox
checked={getCurrentRow(row.index)?.inqAuthrtYn === "Y"}
onCheckedChange={handleCheckedChange(row.index, "inqAuthrtYn")}
/>
);
},
},
];
};
@@ -1,26 +0,0 @@
import { useMenuStore } from "@/shared/lib";
import { Button, cn, type ButtonProps } from "@common/components";
import type { PageMenuAuthority } from "../model/menu-authority-interface";
type AuthorityButtonProps = {
authority: keyof PageMenuAuthority;
} & ButtonProps;
function AuthorityButton({
authority,
className,
children,
...props
}: AuthorityButtonProps) {
const pageMenuAuthority = useMenuStore((state) => state.pageMenuAuthority);
const isAuthority = pageMenuAuthority?.[authority] === "Y";
return (
<Button className={cn(className, !isAuthority && "hidden")} {...props}>
{children}
</Button>
);
}
export { AuthorityButton };
@@ -1,101 +0,0 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
DataTable,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import {
menuAuthorityFormSchema,
type MenuAuthorityFormData,
type MenuAuthorityPayload,
} from "../model/menu-authority-interface";
import { createColumns } from "../model/menu-authority-table-columns";
import { MenuAuthorityApi } from "../api/menu-authority-service";
type ManagementAuthorityDialogProps = {
menuCd?: number;
} & React.ComponentProps<typeof Dialog>;
function ManagementAuthorityDialog({
menuCd,
...props
}: ManagementAuthorityDialogProps) {
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { data: menuAuthorityList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }),
enabled: !!menuCd,
});
const { mutateAsync: saveMenuAuthority } = useMutation({
mutationFn: (data: MenuAuthorityPayload[]) =>
MenuAuthorityApi.saveMenuAuthority(data),
onSuccess: () => {
alert("저장되었습니다.");
},
});
const form = useForm<MenuAuthorityFormData>({
resolver: zodResolver(menuAuthorityFormSchema),
values: {
menuAuthority: menuAuthorityList?.content ?? [],
},
});
const table = useReactTable({
data: menuAuthorityList?.content ?? [],
columns: createColumns(form),
getCoreRowModel: getCoreRowModel(),
});
const handleSave = async () => {
if (await confirm("저장하시겠습니까?")) {
const data = form.getValues("menuAuthority");
const filteredData = data.filter((item) => item.mode !== "R");
await saveMenuAuthority(filteredData);
}
};
return (
<Dialog {...props}>
<DialogTrigger asChild>
<Button className="gap-3" disabled={!menuCd}>
<i className="diveicon di-personal-information" />
권한관리
</Button>
</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>그룹별 메뉴 권한</DialogTitle>
</DialogHeader>
<DataTable table={table} />
<DialogFooter className="!justify-center">
<Button size="lg" onClick={handleSave}>
저장
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
export { ManagementAuthorityDialog };
@@ -1,21 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { MenuInfo, MenuRole, MenuService } from "../model/menu-interface";
export class MenuApiService implements MenuService {
async getMenuRoles(siteId: string): Promise<MenuRole[]> {
const response = await client.get<MenuRole[]>(
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
);
return response.data;
}
async getMenuInfo(menuCd: string): Promise<MenuInfo> {
const response = await client.get<MenuInfo>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
@@ -1,3 +0,0 @@
export { MenuApi } from "./api/menu-service";
export { MenuWrapper } from "./lib/menu-wrapper";
export type { MenuRole } from "./model/menu-interface";
@@ -1,60 +0,0 @@
"use client";
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { MenuAuthorityApi } from "@/entities/menu-authority/@x/menu";
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
import { MenuApi } from "../api/menu-service";
import { getCurrentMenu } from "./utils";
const siteId = getCookie("site-id-key") ?? "";
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
const pathname = usePathname();
const alert = useAlertStore((state) => state.setMessage);
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
useMenuStore((state) => state);
const getCurrentMenuInfo = async () => {
const menuRoles = await MenuApi.getMenuRoles(siteId);
if (!menuRoles) return;
return getCurrentMenu(menuRoles, pathname ?? "");
};
const getPageMenuAuthority = async () => {
const menuCd = getCookie("menu-code-key");
if (!menuCd) return;
const pageMenuAuthority = await MenuAuthorityApi.getPageMenuAuthority(
parseInt(menuCd),
);
return pageMenuAuthority;
};
const checkInquiryAuthority = () => {
return pageMenuAuthority?.inqAuthrtYn === "Y";
};
useEffect(() => {
(async () => {
const currentMenu = await getCurrentMenuInfo();
if (currentMenu) setCurrentMenuInfo(currentMenu);
const pageMenuAuthority = await getPageMenuAuthority();
if (pageMenuAuthority?.length)
setPageMenuAuthority(pageMenuAuthority[0]!);
})();
}, [pathname]);
useEffect(() => {
if (!pageMenuAuthority) return;
const inquiryAuthority = checkInquiryAuthority();
if (!inquiryAuthority)
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
}, [pageMenuAuthority]);
return <>{children}</>;
};
export { MenuWrapper };
@@ -1,20 +0,0 @@
import type { MenuRole } from "../model/menu-interface";
export const getCurrentMenu = (
menus: MenuRole[],
pathname: string,
): MenuRole | null => {
for (const menu of menus) {
if (
menu.urlPath !== "" &&
(menu.urlPath === pathname || pathname.startsWith(menu.urlPath))
)
return menu;
if (menu.children) {
const children = getCurrentMenu(menu.children, pathname);
if (children) return children;
}
}
return null;
};
@@ -1,41 +0,0 @@
interface Menu {
menuCd: number;
menuKornNm: string;
menuEngNm: string;
sortSeq: number;
levelNo: number;
urlPath: string;
menuLnkgTypeCd: string;
menuLnkgSn: number;
menuPath: string;
qrcd: string;
useYn: string;
otptYn: string;
newWndwYn: string;
}
export interface MenuRole extends Menu {
parentId: number | null;
iconNm?: string;
children: MenuRole[] | null;
requiredUrlPath: boolean;
}
export interface MenuInfo extends Menu {
siteId: string;
menuSubKornNm: string;
menuSubEngNm: string;
iconName: string;
menuLnkgTypeCdNm: string;
prntMenuCd: number;
evlUseYn: string;
menuLnkgSnNm: string;
menuPicUserUnqId: string;
menuPicUserUnqIdNm: string;
menuPicDsgnYmd: string;
}
export interface MenuService {
getMenuRoles(siteId: string): Promise<MenuRole[]>;
getMenuInfo(menuCd: string): Promise<MenuInfo>;
}
@@ -1,66 +0,0 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
type Organization,
type OrganizationFormData,
type OrganizationService,
type OrganizationTree,
type OrganizationTreeForDnd,
} from "../model/organization-interface";
export class OrganizationApiService implements OrganizationService {
public async getOrganizationsTreeList(): Promise<OrganizationTree[]> {
const response = await client.get<OrganizationTree[]>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/tree`,
);
return response.data;
}
public async getOrganizationInfo(ognzSn: number): Promise<Organization> {
const response = await client.get<Organization>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
);
return response.data;
}
public async createOrganization(data: OrganizationFormData): Promise<void> {
await client.post<void>(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, data);
}
public async deleteOrganization(ognzSn: number): Promise<void> {
await client.delete<void>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
);
}
public async updateOrganizationName(
ognzSn: number,
name: string,
): Promise<Organization> {
const response = await client.put<Organization>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}/${name}`,
);
return response.data;
}
public async updateOrganization(
ognzSn: number,
data: OrganizationFormData,
): Promise<void> {
await client.put<void>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
data,
);
}
public async updateOrganizationByDnd(
data: OrganizationTreeForDnd[],
): Promise<void> {
await client.put<void>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/dnd/tree`,
data,
);
}
}
export const OrganizationApi = new OrganizationApiService();
@@ -1,8 +0,0 @@
export { OrganizationApi } from "./api/organization-service";
export {
organizationFormSchema,
type OrganizationTree,
type OrganizationFormData,
type OrganizationTreeForDnd,
} from "./model/organization-interface";
export { generateOrganizationTreeData, handleDropNode } from "./lib/utils";

Some files were not shown because too many files have changed in this diff Show More