Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.next
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env*
|
||||
apps/**/node_modules
|
||||
packages/**/node_modules
|
||||
apps/**/.next
|
||||
**/.next/cache
|
||||
@@ -1,19 +0,0 @@
|
||||
name: iipbs_wr
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [iipbs/wr/dev]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: appleboy/ssh-action@master
|
||||
with:
|
||||
port: 11022
|
||||
host: con.mind-one.co.kr
|
||||
username: mindone
|
||||
password: mindone!23
|
||||
script: |
|
||||
cd ./iipbs/front
|
||||
sh ./frontStartWr.sh
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# ---------------------- #
|
||||
# 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"]
|
||||
@@ -110,15 +110,15 @@ const nextConfig: NextConfig = {
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
value: "(.*)/cms(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
source: "/cms/:path*",
|
||||
destination: `${subDomain}/cms/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
@@ -127,6 +127,7 @@ const nextConfig: NextConfig = {
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
```
|
||||
|
||||
### 6. globals.css(또는 전역 css) 수정
|
||||
@@ -150,6 +151,26 @@ 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)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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)} />;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"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
-1
@@ -1,4 +1,4 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/faq";
|
||||
import { ModifyPage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqPostModifyPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { FaqPage } from "@/pages/(pst)/faq";
|
||||
import { FaqPage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/faq";
|
||||
import { CreatePage } from "@/pages/(post)/faq";
|
||||
|
||||
export default async function PstBbsFaqRegisterPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/image";
|
||||
import { ModifyPage } from "@/pages/(post)/image";
|
||||
|
||||
export default async function PstBbsImagePostModifyPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ImagePost } from "@/pages/(pst)/image";
|
||||
import { ImagePost } from "@/pages/(post)/image";
|
||||
|
||||
export default async function PstBbsImagePostPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ImagePage } from "@/pages/(pst)/image";
|
||||
import { ImagePage } from "@/pages/(post)/image";
|
||||
|
||||
export default async function PstBbsImagePage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/image";
|
||||
import { CreatePage } from "@/pages/(post)/image";
|
||||
|
||||
export default async function PstBbsImageRegisterPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/movie";
|
||||
import { ModifyPage } from "@/pages/(post)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostModifyPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { MoviePost } from "@/pages/(pst)/movie";
|
||||
import { MoviePost } from "@/pages/(post)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { MoviePage } from "@/pages/(pst)/movie";
|
||||
import { MoviePage } from "@/pages/(post)/movie";
|
||||
|
||||
export default async function PstBbsMoviePage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/movie";
|
||||
import { CreatePage } from "@/pages/(post)/movie";
|
||||
|
||||
export default async function PstBbsMovieRegisterPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/normal";
|
||||
import { CreatePage } from "@/pages/(post)/normal";
|
||||
|
||||
export default async function PstBbsNormalAnswerPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/normal";
|
||||
import { ModifyPage } from "@/pages/(post)/normal";
|
||||
|
||||
export default async function PstBbsNormalPostModifyPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { NormalPost } from "@/pages/(pst)/normal";
|
||||
import { NormalPost } from "@/pages/(post)/normal";
|
||||
|
||||
export default async function PstBbsNormalPostPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { NormalPage } from "@/pages/(pst)/normal";
|
||||
import { NormalPage } from "@/pages/(post)/normal";
|
||||
|
||||
export default async function PstBbsNormalPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/normal";
|
||||
import { CreatePage } from "@/pages/(post)/normal";
|
||||
|
||||
export default async function PstBbsNormalRegisterPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/notice";
|
||||
import { ModifyPage } from "@/pages/(post)/notice";
|
||||
|
||||
export default async function PstBbsNoticePostModifyPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { NoticePost } from "@/pages/(pst)/notice";
|
||||
import { NoticePost } from "@/pages/(post)/notice";
|
||||
|
||||
export default async function PstBbsNoticePostPage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { NoticePage } from "@/pages/(pst)/notice";
|
||||
import { NoticePage } from "@/pages/(post)/notice";
|
||||
|
||||
export default async function PstBbsNoticePage({
|
||||
params,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CreatePage } from "@/pages/(pst)/notice";
|
||||
import { CreatePage } from "@/pages/(post)/notice";
|
||||
|
||||
export default async function PstBbsNoticeRegisterPage({
|
||||
params,
|
||||
@@ -0,0 +1,28 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import type { MenuRole } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
@@ -17,7 +17,7 @@ export default function WithBaseLayout({
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
const handleMenuClick = (item: MenuRole) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function PostLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={bbsInfo?.bbsNm ?? ""}
|
||||
menuPaths={["서비스관리", "게시판관리"]}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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 });
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
CLAIM_NAME,
|
||||
REFRESH_TOKEN,
|
||||
SERVER_API_URL,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
@@ -15,9 +14,10 @@ async function handler(
|
||||
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": SITE_ID,
|
||||
"X-Site-Id": siteId,
|
||||
};
|
||||
let body = {};
|
||||
const isRefreshHandler = params.url.includes("refresh");
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
const nextConfig: NextConfig = {
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/cms/:path*",
|
||||
destination: "/:path*",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -29,9 +29,11 @@
|
||||
"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",
|
||||
@@ -39,7 +41,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
|
||||
@@ -5,10 +5,11 @@ 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 { StoreProvider, PromptProvider } from "@/shared/providers";
|
||||
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();
|
||||
@@ -22,6 +23,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
<Toaster position="top-center" />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
|
||||
@@ -19,6 +19,17 @@ export class ContentsApiService implements ContentsService {
|
||||
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> {
|
||||
|
||||
@@ -65,6 +65,7 @@ export const contentsDetailSchema = z.object({
|
||||
aprvDt: z.string(),
|
||||
autzr: z.string(),
|
||||
useYn: z.string(),
|
||||
menuPicUserUnqIdNm: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const contentsFormSchema = contentsDetailSchema.omit({
|
||||
@@ -81,6 +82,10 @@ 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>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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,14 +1,21 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { MenuItem, MenuService } from "../model/menu-interface";
|
||||
import type { MenuInfo, MenuRole, MenuService } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
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,2 +1,3 @@
|
||||
export { MenuApi } from "./api/menu-service";
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
export type { MenuItem } from "./model/menu-interface";
|
||||
export type { MenuRole } from "./model/menu-interface";
|
||||
|
||||
@@ -4,10 +4,11 @@ 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 { SITE_ID } from "@/shared/config";
|
||||
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();
|
||||
@@ -17,7 +18,7 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
const menuRoles = await MenuApi.getMenuRoles(siteId);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
@@ -44,7 +45,7 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
if (pageMenuAuthority?.length)
|
||||
setPageMenuAuthority(pageMenuAuthority[0]!);
|
||||
})();
|
||||
}, []);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageMenuAuthority) return;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MenuItem } from "../model/menu-interface";
|
||||
import type { MenuRole } from "../model/menu-interface";
|
||||
|
||||
export const getCurrentMenu = (
|
||||
menus: MenuItem[],
|
||||
menus: MenuRole[],
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
): MenuRole | null => {
|
||||
for (const menu of menus) {
|
||||
if (
|
||||
menu.urlPath !== "" &&
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
export interface MenuItem {
|
||||
interface Menu {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
@@ -15,9 +12,30 @@ export interface MenuItem {
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
}
|
||||
|
||||
export interface MenuRole extends Menu {
|
||||
parentId: number | null;
|
||||
iconNm?: string;
|
||||
children: MenuRole[] | null;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuItem[]>;
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,16 @@ export class PostApiService implements PostService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostEditInfo(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/edit/${bbsId}/${pstNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createPost(data: PostFormData): Promise<Post> {
|
||||
const validatedData = postFormSchema.parse(data);
|
||||
const response = await client.post<Post>(
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PostParams extends PageableParams {
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
ansUseYn?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
@@ -77,6 +78,7 @@ export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
export interface PostService {
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
getPostEditInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
bbsId: number,
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
+1
-1
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
+1
-1
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
+1
-1
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
+6
@@ -8,6 +8,12 @@ export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row, table }) => {
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
+1
-1
@@ -20,7 +20,7 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
queryFn: () => PostApi.getPostEditInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
@@ -0,0 +1,59 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
MenuEvaluation,
|
||||
MenuEvaluationDetail,
|
||||
MenuEvaluationParams,
|
||||
MenuEvaluationService,
|
||||
MenuEvaluationCreateRequestDto,
|
||||
MenuEvaluationUpdateRequestDto,
|
||||
PageMenuEvaluationListResponseDto,
|
||||
} from "../model/menu-evaluation-interface";
|
||||
|
||||
export class MenuEvaluationApiService implements MenuEvaluationService {
|
||||
public async getMenuEvaluationList(
|
||||
params: MenuEvaluationParams,
|
||||
): Promise<PageMenuEvaluationListResponseDto> {
|
||||
const response = await client.get<PageMenuEvaluationListResponseDto>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getMenuEvaluation(
|
||||
menuEvlSn: number,
|
||||
): Promise<MenuEvaluationDetail> {
|
||||
const response = await client.get<MenuEvaluationDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createMenuEvaluation(
|
||||
data: MenuEvaluationCreateRequestDto,
|
||||
): Promise<MenuEvaluation> {
|
||||
const response = await client.post<MenuEvaluation>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenuEvaluation(
|
||||
menuEvlSn: number,
|
||||
data: MenuEvaluationUpdateRequestDto,
|
||||
): Promise<MenuEvaluation> {
|
||||
const response = await client.put<MenuEvaluation>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteMenuEvaluation(menuEvlSn: number): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.MENU_EVALUATION}/${menuEvlSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuEvaluationApi = new MenuEvaluationApiService();
|
||||
@@ -0,0 +1,66 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
MenuModificationDemand,
|
||||
MenuModificationDemandCreateRequestDto,
|
||||
MenuModificationDemandDetail,
|
||||
MenuModificationDemandParams,
|
||||
MenuModificationDemandService,
|
||||
MenuModificationDemandUpdateRequestDto,
|
||||
PageMenuModificationDemandListResponseDto,
|
||||
} from "../model/menu-modification-demand-interface";
|
||||
|
||||
export class MenuModificationDemandApiService
|
||||
implements MenuModificationDemandService
|
||||
{
|
||||
public async getMenuModificationDemandList(
|
||||
params: MenuModificationDemandParams,
|
||||
): Promise<PageMenuModificationDemandListResponseDto> {
|
||||
const response =
|
||||
await client.get<PageMenuModificationDemandListResponseDto>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getMenuModificationDemand(
|
||||
mdfcnRcptNo: number,
|
||||
): Promise<MenuModificationDemandDetail> {
|
||||
const response = await client.get<MenuModificationDemandDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createMenuModificationDemand(
|
||||
data: MenuModificationDemandCreateRequestDto,
|
||||
): Promise<MenuModificationDemand> {
|
||||
const response = await client.post<MenuModificationDemand>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenuModificationDemand(
|
||||
mdfcnRcptNo: number,
|
||||
data: MenuModificationDemandUpdateRequestDto,
|
||||
): Promise<MenuModificationDemand> {
|
||||
const response = await client.put<MenuModificationDemand>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteMenuModificationDemand(
|
||||
mdfcnRcptNo: number,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_MODIFICATION_DEMAND}/${mdfcnRcptNo}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuModificationDemandApi = new MenuModificationDemandApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentsPage } from "./ui/contents-page";
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface MenuEvaluationParams extends PageableParams {
|
||||
menuCd?: number;
|
||||
siteId?: string;
|
||||
evlIp?: string;
|
||||
}
|
||||
|
||||
export interface MenuEvaluation {
|
||||
evlSn: number;
|
||||
menuCd: number;
|
||||
siteId: string;
|
||||
evlDt: string;
|
||||
evlScr: number;
|
||||
}
|
||||
|
||||
export interface MenuEvaluationDetail extends MenuEvaluation {
|
||||
evlIp: string;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
export interface MenuEvaluationCreateRequestDto {
|
||||
menuCd: number;
|
||||
siteId: string;
|
||||
evlIp?: string;
|
||||
evlScr: number;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
export interface MenuEvaluationUpdateRequestDto {
|
||||
evlScr: number;
|
||||
}
|
||||
|
||||
export type PageMenuEvaluationListResponseDto = PageableList<MenuEvaluation>;
|
||||
|
||||
export interface MenuEvaluationService {
|
||||
getMenuEvaluationList(
|
||||
params: MenuEvaluationParams,
|
||||
): Promise<PageMenuEvaluationListResponseDto>;
|
||||
getMenuEvaluation(menuEvlSn: number): Promise<MenuEvaluationDetail>;
|
||||
createMenuEvaluation(
|
||||
data: MenuEvaluationCreateRequestDto,
|
||||
): Promise<MenuEvaluation>;
|
||||
updateMenuEvaluation(
|
||||
menuEvlSn: number,
|
||||
data: MenuEvaluationUpdateRequestDto,
|
||||
): Promise<MenuEvaluation>;
|
||||
deleteMenuEvaluation(menuEvlSn: number): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface MenuModificationDemandParams extends PageableParams {
|
||||
menuCd?: number;
|
||||
siteId?: string;
|
||||
mdfcnDmndSttusCd?: string;
|
||||
}
|
||||
|
||||
export interface MenuModificationDemand {
|
||||
mdfcnRcptNo: number;
|
||||
menuCd: number;
|
||||
rcptDt: string;
|
||||
aplcnt: string;
|
||||
aplcnYn: string;
|
||||
}
|
||||
|
||||
export interface MenuModificationDemandDetail extends MenuModificationDemand {
|
||||
mdfcnCn: string;
|
||||
userUnqId: string;
|
||||
aplcntEml: string;
|
||||
}
|
||||
|
||||
export const menuModificationDemandCreateRequestDtoSchema = z.object({
|
||||
menuCd: z.number(),
|
||||
mdfcnCn: z.string(),
|
||||
userUnqId: z.string().optional(),
|
||||
aplcnt: z.string(),
|
||||
aplcntEml: z.string().optional(),
|
||||
});
|
||||
|
||||
export const menuModificationDemandUpdateRequestDtoSchema = z.object({
|
||||
mdfcnCn: z.string(),
|
||||
aplcntEml: z.string().optional(),
|
||||
aplcnYn: z.string(),
|
||||
});
|
||||
|
||||
export type MenuModificationDemandCreateRequestDto = z.infer<
|
||||
typeof menuModificationDemandCreateRequestDtoSchema
|
||||
>;
|
||||
|
||||
export type MenuModificationDemandUpdateRequestDto = z.infer<
|
||||
typeof menuModificationDemandUpdateRequestDtoSchema
|
||||
>;
|
||||
|
||||
export type PageMenuModificationDemandListResponseDto =
|
||||
PageableList<MenuModificationDemand>;
|
||||
|
||||
export interface MenuModificationDemandService {
|
||||
getMenuModificationDemandList(
|
||||
params: MenuModificationDemandParams,
|
||||
): Promise<PageMenuModificationDemandListResponseDto>;
|
||||
|
||||
getMenuModificationDemand(
|
||||
mdfcnRcptNo: number,
|
||||
): Promise<MenuModificationDemandDetail>;
|
||||
|
||||
createMenuModificationDemand(
|
||||
data: MenuModificationDemandCreateRequestDto,
|
||||
): Promise<MenuModificationDemand>;
|
||||
|
||||
updateMenuModificationDemand(
|
||||
mdfcnRcptNo: number,
|
||||
data: MenuModificationDemandUpdateRequestDto,
|
||||
): Promise<MenuModificationDemand>;
|
||||
|
||||
deleteMenuModificationDemand(mdfcnRcptNo: number): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import { toast } from "sonner";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useClipboard } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
getCookie,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@common/components";
|
||||
import { CodeApi } from "@/entities/code";
|
||||
import { ContentsApi } from "@/entities/content";
|
||||
import { AUTH_USER_ID, ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { MenuEvaluationApi } from "../api/menu-evaluation-service";
|
||||
import { MenuModificationDemandDialog } from "./menu-modification-demand-dialog";
|
||||
|
||||
function ContentsPage({ contsSn }: { contsSn: number }) {
|
||||
const [evlScr, setEvlScr] = useState(5);
|
||||
const shadowRootRef = useRef<HTMLDivElement>(null);
|
||||
const { copyText, copied } = useClipboard();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
const userUnqId = getCookie(AUTH_USER_ID) ?? "";
|
||||
|
||||
const { data: contentsDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
queryFn: () =>
|
||||
ContentsApi.getLatestApprovedContentsInfo(
|
||||
contsSn,
|
||||
currentMenuInfo?.menuCd.toString() ?? "",
|
||||
),
|
||||
enabled: !!contsSn && !!currentMenuInfo,
|
||||
});
|
||||
|
||||
const { data: koglData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"],
|
||||
queryFn: () => CodeApi.getCode("KOGLCD"),
|
||||
select: (data) =>
|
||||
data.find((code) => code.lwpoCd === contentsDetail?.koglCd),
|
||||
enabled: !!contentsDetail,
|
||||
});
|
||||
|
||||
const { data: cclData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"],
|
||||
queryFn: () => CodeApi.getCode("CCLCD"),
|
||||
select: (data) =>
|
||||
data.find((code) => code.lwpoCd === contentsDetail?.cclCd),
|
||||
enabled: !!contentsDetail,
|
||||
});
|
||||
|
||||
const { mutate: menuEvaluation } = useMutation({
|
||||
mutationFn: (evlScr: number) =>
|
||||
MenuEvaluationApi.createMenuEvaluation({
|
||||
menuCd: currentMenuInfo?.menuCd ?? 0,
|
||||
siteId,
|
||||
evlScr,
|
||||
userUnqId,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
alert("게시물 평가가 완료되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!shadowRootRef.current || !contentsDetail?.contsCn) return;
|
||||
|
||||
const shadowRoot = shadowRootRef.current.attachShadow({ mode: "open" });
|
||||
|
||||
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 = contentsDetail.contsCn;
|
||||
shadowRoot.appendChild(contentElement);
|
||||
}, [contentsDetail?.contsCn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) toast.success("URL이 클립보드에 복사되었습니다.");
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card card-border !mt-6 !p-6">
|
||||
<div ref={shadowRootRef} />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="card card-border flex flex-1 items-center gap-2">
|
||||
<div className="rounded-sm border border-border bg-accent px-8 py-4">
|
||||
<RadioGroup
|
||||
value={evlScr.toString()}
|
||||
onValueChange={(value) => setEvlScr(parseInt(value))}
|
||||
className="flex items-center space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="5" id="evl5" />
|
||||
<Label htmlFor="evl5">매우 만족 😊</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="4" id="evl4" />
|
||||
<Label htmlFor="evl4">만족 🙂</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="3" id="evl3" />
|
||||
<Label htmlFor="evl3">보통 😐</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="2" id="evl2" />
|
||||
<Label htmlFor="evl2">불만족 🙁</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="1" id="evl1" />
|
||||
<Label htmlFor="evl1">매우 불만족 😞</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
onClick={() => menuEvaluation(evlScr)}
|
||||
>
|
||||
평가하기
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border flex flex-1 items-center gap-4">
|
||||
<div className="rounded-sm border border-border bg-accent px-8 py-4">
|
||||
최종수정일: {dayjs(contentsDetail?.aprvDt).format("YYYY-MM-DD")} |
|
||||
담당자: {contentsDetail?.menuPicUserUnqIdNm}
|
||||
</div>
|
||||
<Image
|
||||
src={`/images/${cclData?.unit}`}
|
||||
alt="CCL"
|
||||
width={114}
|
||||
height={40}
|
||||
/>
|
||||
<Image
|
||||
src={`/images/${koglData?.unit}`}
|
||||
alt="CCL"
|
||||
width={140}
|
||||
height={40}
|
||||
/>
|
||||
<Button
|
||||
size="lg"
|
||||
color="emerald"
|
||||
onClick={() => copyText(window.location.href)}
|
||||
>
|
||||
퍼가기
|
||||
</Button>
|
||||
<MenuModificationDemandDialog menuCd={currentMenuInfo?.menuCd} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentsPage };
|
||||
@@ -0,0 +1,173 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Input,
|
||||
DialogTrigger,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
|
||||
import { MenuModificationDemandApi } from "../api/menu-modification-demand-service";
|
||||
import {
|
||||
menuModificationDemandCreateRequestDtoSchema,
|
||||
type MenuModificationDemandCreateRequestDto,
|
||||
} from "../model/menu-modification-demand-interface";
|
||||
|
||||
type MenuModificationDemandDialogProps = {
|
||||
menuCd?: number;
|
||||
};
|
||||
|
||||
const MenuModificationDemandDialog = memo(
|
||||
({ menuCd }: MenuModificationDemandDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createMenuModificationDemand } = useMutation({
|
||||
mutationFn: (data: MenuModificationDemandCreateRequestDto) =>
|
||||
MenuModificationDemandApi.createMenuModificationDemand(data),
|
||||
onSuccess: () => {
|
||||
alert("메뉴 수정 요청이 등록되었습니다.", () => setOpen(false));
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuModificationDemandCreateRequestDto>({
|
||||
resolver: zodResolver(menuModificationDemandCreateRequestDtoSchema),
|
||||
defaultValues: {
|
||||
menuCd: menuCd ?? 0,
|
||||
mdfcnCn: "",
|
||||
aplcnt: "",
|
||||
aplcntEml: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: MenuModificationDemandCreateRequestDto) => {
|
||||
if (await confirm("메뉴 수정 요청을 등록하시겠습니까?")) {
|
||||
createMenuModificationDemand(data);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) form.reset();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="lg" color="white">
|
||||
수정요청
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>메뉴 수정 요청</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>신청자</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="aplcnt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="신청자명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="aplcntEml"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이메일을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>요청 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mdfcnCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="요청 내용을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" type="submit">
|
||||
요청하기
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MenuModificationDemandDialog.displayName = "MenuModificationDemandDialog";
|
||||
|
||||
export { MenuModificationDemandDialog };
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Client } from "@common/network";
|
||||
import { BASE_URL } from "../config/api";
|
||||
import { SITE_ID } from "../config/env";
|
||||
import { getCookie } from "../lib";
|
||||
|
||||
const siteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
export const client = Client.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
siteId: SITE_ID,
|
||||
siteId,
|
||||
});
|
||||
|
||||
@@ -28,6 +28,8 @@ export const ENDPOINT = {
|
||||
MENUS: "/cms-service/api/v1/menus",
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
MENU_AUTHRTS: "/cms-service/api/v1/menu-authrts",
|
||||
MENU_EVALUATION: "/cms-service/api/v1/menu-evls",
|
||||
MENU_MODIFICATION_DEMAND: "/cms-service/api/v1/menu-mdfcn-dmnds",
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
CONTENTS_DETAILS: "/cms-service/api/v1/contents-details",
|
||||
ORGANIZATIONS: "/cms-service/api/v1/ognzs",
|
||||
@@ -41,6 +43,7 @@ export const ENDPOINT = {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
POSTS_USER: "/board-service/api/v1/posts/user",
|
||||
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
|
||||
POST_EVALUATION: "/board-service/api/v1/pstEvl",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "13000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN =
|
||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
|
||||
|
||||
@@ -30,5 +30,5 @@ export interface PageableList<T> {
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string[];
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
PostEvaluationPayload,
|
||||
PostEvaluationService,
|
||||
} from "../model/post-evaluation-interface";
|
||||
|
||||
export class PostEvaluationApiService implements PostEvaluationService {
|
||||
public async postEvaluation(payload: PostEvaluationPayload) {
|
||||
await client.post(ENDPOINT.BOARD_SERVICE.POST_EVALUATION, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export const PostEvaluationApi = new PostEvaluationApiService();
|
||||
@@ -1,3 +1,4 @@
|
||||
export { Post } from "./ui/post";
|
||||
export { PostForm } from "./ui/post-form";
|
||||
export { PostPage } from "./ui/post-page";
|
||||
export { SortSelect } from "./ui/sort-select";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface PostEvaluationPayload {
|
||||
pstNo: number;
|
||||
evlIp?: string;
|
||||
evlDt?: string;
|
||||
evlScr: number;
|
||||
}
|
||||
|
||||
export interface PostEvaluationService {
|
||||
postEvaluation: (payload: PostEvaluationPayload) => Promise<void>;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row }) => {
|
||||
cell: ({ row, table }) => {
|
||||
if (row.original.isUpndFixPost) {
|
||||
return (
|
||||
<Badge
|
||||
@@ -40,19 +40,23 @@ export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return row.original.rowNo;
|
||||
const totalRows = table.getRowCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const pageSize = table.getState().pagination.pageSize;
|
||||
return totalRows - (currentPage * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => {
|
||||
const { ansPstn, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const { ansPstn = 0, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
const indentPadding = ansPstn > 0 ? (ansPstn - 1) * 20 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-[20vw] items-center gap-2 text-left"
|
||||
style={{ paddingInlineStart: `${(ansPstn - 1) * 20}px` }}
|
||||
style={{ paddingInlineStart: `${indentPadding}px` }}
|
||||
>
|
||||
{ansPstn > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { toast } from "sonner";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useClipboard, useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { AuthorityButton } from "@/entities/menu-authority";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
pstNo: string;
|
||||
registerId?: string;
|
||||
children: React.ReactNode;
|
||||
handleDelete: () => Promise<void>;
|
||||
};
|
||||
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
function PostAction({ registerId, children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const { copyText, copied } = useClipboard();
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const actionButtonAuthrt =
|
||||
userInfo?.adminRoleYn === "Y" || userInfo?.userUnqId === registerId;
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) handleDelete();
|
||||
};
|
||||
@@ -34,10 +40,22 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) toast.success("URL이 클립보드에 복사되었습니다.");
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="emerald"
|
||||
type="button"
|
||||
onClick={() => copyText(window.location.href)}
|
||||
>
|
||||
퍼가기
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
@@ -46,15 +64,19 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => router.push(`${pathname}/modify`)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
{actionButtonAuthrt && (
|
||||
<AuthorityButton
|
||||
authority="mdfcnAuthrtYn"
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => router.push(`${pathname}/modify`)}
|
||||
>
|
||||
수정
|
||||
</AuthorityButton>
|
||||
)}
|
||||
{bbsInfo?.ansUseYn === "Y" && (
|
||||
<Button
|
||||
<AuthorityButton
|
||||
authority="regAuthrtYn"
|
||||
size="lg"
|
||||
type="button"
|
||||
color="gray"
|
||||
@@ -62,11 +84,19 @@ function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
disabled={!checkAnsUseAuthrt()}
|
||||
>
|
||||
답글
|
||||
</Button>
|
||||
</AuthorityButton>
|
||||
)}
|
||||
{actionButtonAuthrt && (
|
||||
<AuthorityButton
|
||||
authority="delAuthrtYn"
|
||||
size="lg"
|
||||
color="red"
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
>
|
||||
삭제
|
||||
</AuthorityButton>
|
||||
)}
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -77,7 +77,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>제목</TableHead>
|
||||
<TableHead data-required>제목</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -120,7 +120,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
</TableRow>
|
||||
{!userInfo?.userUnqId && (
|
||||
<TableRow>
|
||||
<TableHead>비밀번호</TableHead>
|
||||
<TableHead data-required>비밀번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { GridViewDataTable } from "@/shared/ui/grid-view-data-table";
|
||||
import { columns as tableColumns } from "../model/post-table-columns";
|
||||
import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns";
|
||||
import { SortSelect } from "./sort-select";
|
||||
|
||||
function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
@@ -42,7 +43,10 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
const thumbRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
const [tableSort, setTableSort] = useState("");
|
||||
const [keywordType, setKeywordType] = useState("title");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [searchParams, setSearchParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
});
|
||||
@@ -56,7 +60,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
...searchParams,
|
||||
upndFixYn: bbsInfo?.upndFixUseYn,
|
||||
}),
|
||||
select: (data) => {
|
||||
@@ -77,12 +81,20 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId],
|
||||
queryKey: [
|
||||
ENDPOINT.BOARD_SERVICE.POSTS_USER,
|
||||
bbsId,
|
||||
searchParams,
|
||||
pagination,
|
||||
tableSort,
|
||||
],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
...searchParams,
|
||||
ansUseYn: bbsInfo?.ansUseYn,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: tableSort,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -124,7 +136,29 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType,
|
||||
keyword,
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setKeywordType("title");
|
||||
setKeyword("");
|
||||
setPagination({
|
||||
...pagination,
|
||||
pageIndex: 0,
|
||||
});
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Post) => {
|
||||
@@ -137,12 +171,7 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-2 p-2">
|
||||
<Label className="mr-2 text-label">검색</Label>
|
||||
<Select
|
||||
value={postParams.keywordType}
|
||||
onValueChange={(keywordType) => {
|
||||
setPostParams({ ...postParams, keywordType });
|
||||
}}
|
||||
>
|
||||
<Select value={keywordType} onValueChange={setKeywordType}>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
@@ -153,15 +182,21 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
className="h-[3.2rem]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
@@ -200,6 +235,9 @@ function PostPage({ bbsId }: { bbsId: string }) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{bbsInfo?.ansUseYn === "N" && (
|
||||
<SortSelect value={tableSort} onValueChange={setTableSort} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,13 @@ import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Button, Skeleton } from "@common/components";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi } from "@/entities/post";
|
||||
import {
|
||||
@@ -17,6 +23,7 @@ import {
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PostAction } from "./post-action";
|
||||
import { PostComment } from "./post-comment";
|
||||
import { PostEvaluationApi } from "../api/post-evaluation-service";
|
||||
|
||||
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
@@ -26,6 +33,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
|
||||
const [filename, setFilename] = useState("");
|
||||
const [evlScr, setEvlScr] = useState(5);
|
||||
|
||||
const { data: postData } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
@@ -73,6 +81,17 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: postEvaluation } = useMutation({
|
||||
mutationFn: (evlScr: number) =>
|
||||
PostEvaluationApi.postEvaluation({
|
||||
pstNo: parseInt(pstNo),
|
||||
evlScr,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
alert("게시물 평가가 완료되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
let pstPswd = "";
|
||||
if (userInfo?.userUnqId) {
|
||||
@@ -125,7 +144,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostAction bbsId={bbsId} pstNo={pstNo} handleDelete={handleDelete}>
|
||||
<PostAction registerId={postData?.rgtr} handleDelete={handleDelete}>
|
||||
<div className="card card-border !p-8">
|
||||
<h3 className="text-[2rem] font-bold text-foreground">
|
||||
{postData?.pstTtl}
|
||||
@@ -194,6 +213,53 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
</>
|
||||
)
|
||||
)}
|
||||
{bbsInfo?.dgstfnEvlUseYn === "Y" && (
|
||||
<>
|
||||
<hr />
|
||||
<div className="mt-8 flex items-center gap-2 px-4">
|
||||
<p>
|
||||
이 페이지에서 제공하는
|
||||
<br />
|
||||
정보에 대하여 만족하시나요?
|
||||
</p>
|
||||
<div className="ml-4 rounded-sm border border-border bg-accent px-8 py-4">
|
||||
<RadioGroup
|
||||
value={evlScr.toString()}
|
||||
onValueChange={(value) => setEvlScr(parseInt(value))}
|
||||
className="flex items-center space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="5" id="evl5" />
|
||||
<Label htmlFor="evl5">매우 만족 😊</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="4" id="evl4" />
|
||||
<Label htmlFor="evl4">만족 🙂</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="3" id="evl3" />
|
||||
<Label htmlFor="evl3">보통 😐</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="2" id="evl2" />
|
||||
<Label htmlFor="evl2">불만족 🙁</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="1" id="evl1" />
|
||||
<Label htmlFor="evl1">매우 불만족 😞</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
onClick={() => postEvaluation(evlScr)}
|
||||
>
|
||||
평가하기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PostAction>
|
||||
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "@common/components";
|
||||
|
||||
type SortSelectProps = {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
};
|
||||
|
||||
function SortSelect(props: SortSelectProps) {
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="h-[3.2rem] w-[8rem]">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pstTtl,asc">제목↑</SelectItem>
|
||||
<SelectItem value="pstTtl,desc">제목↓</SelectItem>
|
||||
<SelectItem value="rgtrNm,asc">작성자↑</SelectItem>
|
||||
<SelectItem value="rgtrNm,desc">작성자↓</SelectItem>
|
||||
<SelectItem value="regDt,asc">등록일↑</SelectItem>
|
||||
<SelectItem value="regDt,desc">등록일↓</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export { SortSelect };
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
PORT=13000
|
||||
LOG_FILE="start_13000.log"
|
||||
|
||||
# 1. 기존 포트 사용 프로세스 종료
|
||||
PID=$(lsof -t -i :$PORT)
|
||||
if [ -n "$PID" ]; then
|
||||
echo "Killing process on port $PORT (PID: $PID)"
|
||||
kill -9 $PID
|
||||
fi
|
||||
|
||||
# 2. 백그라운드로 앱 시작 (로그 파일로 리디렉션)
|
||||
echo "Starting app on port $PORT..."
|
||||
nohup pnpm start > $LOG_FILE 2>&1 &
|
||||
|
||||
# 3. PID 확인
|
||||
sleep 1
|
||||
NEW_PID=$(lsof -t -i :$PORT)
|
||||
if [ -n "$NEW_PID" ]; then
|
||||
echo "App started successfully (PID: $NEW_PID)"
|
||||
echo "Logs: tail -f $LOG_FILE"
|
||||
else
|
||||
echo "Failed to start app. Check logs: $LOG_FILE"
|
||||
fi
|
||||
@@ -0,0 +1,7 @@
|
||||
# apps/cs/src/shared/config/env.ts 참고
|
||||
NEXT_PUBLIC_PORT=3000
|
||||
NEXT_PUBLIC_PROXY_HOST=http://localhost:3000
|
||||
NEXT_PUBLIC_SITE_ID=CS
|
||||
NEXT_PUBLIC_SUB_DOMAIN=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000
|
||||
NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2
|
||||
+2
-1
@@ -37,4 +37,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -0,0 +1,2 @@
|
||||
import { AdminPage } from "@/pages/admin";
|
||||
export default AdminPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { ApiLogPage } from "@/pages/apilog";
|
||||
export default ApiLogPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { GroupsPage } from "@/pages/groups";
|
||||
export default GroupsPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginlogPage } from "@/pages/loginlog";
|
||||
export default LoginlogPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { PrivacyLogPage } from "@/pages/privacylog";
|
||||
export default PrivacyLogPage;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user