Merge branch 'iipbs/common/main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/common/main

This commit is contained in:
swcho
2025-07-16 10:03:29 +09:00
633 changed files with 36447 additions and 13778 deletions
+9
View File
@@ -0,0 +1,9 @@
node_modules
.next
Dockerfile
.dockerignore
.env*
apps/**/node_modules
packages/**/node_modules
apps/**/.next
**/.next/cache
-19
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+93
View File
@@ -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"]
+24 -3
View File
@@ -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,4 +1,4 @@
import { ModifyPage } from "@/pages/(pst)/faq";
import { ModifyPage } from "@/pages/(post)/faq";
export default async function PstBbsFaqPostModifyPage({
params,
@@ -1,4 +1,4 @@
import { FaqPage } from "@/pages/(pst)/faq";
import { FaqPage } from "@/pages/(post)/faq";
export default async function PstBbsFaqPage({
params,
@@ -1,4 +1,4 @@
import { CreatePage } from "@/pages/(pst)/faq";
import { CreatePage } from "@/pages/(post)/faq";
export default async function PstBbsFaqRegisterPage({
params,
@@ -1,4 +1,4 @@
import { ModifyPage } from "@/pages/(pst)/image";
import { ModifyPage } from "@/pages/(post)/image";
export default async function PstBbsImagePostModifyPage({
params,
@@ -1,4 +1,4 @@
import { ImagePost } from "@/pages/(pst)/image";
import { ImagePost } from "@/pages/(post)/image";
export default async function PstBbsImagePostPage({
params,
@@ -1,4 +1,4 @@
import { ImagePage } from "@/pages/(pst)/image";
import { ImagePage } from "@/pages/(post)/image";
export default async function PstBbsImagePage({
params,
@@ -1,4 +1,4 @@
import { CreatePage } from "@/pages/(pst)/image";
import { CreatePage } from "@/pages/(post)/image";
export default async function PstBbsImageRegisterPage({
params,
@@ -1,4 +1,4 @@
import { ModifyPage } from "@/pages/(pst)/movie";
import { ModifyPage } from "@/pages/(post)/movie";
export default async function PstBbsMoviePostModifyPage({
params,
@@ -1,4 +1,4 @@
import { MoviePost } from "@/pages/(pst)/movie";
import { MoviePost } from "@/pages/(post)/movie";
export default async function PstBbsMoviePostPage({
params,
@@ -1,4 +1,4 @@
import { MoviePage } from "@/pages/(pst)/movie";
import { MoviePage } from "@/pages/(post)/movie";
export default async function PstBbsMoviePage({
params,
@@ -1,4 +1,4 @@
import { CreatePage } from "@/pages/(pst)/movie";
import { CreatePage } from "@/pages/(post)/movie";
export default async function PstBbsMovieRegisterPage({
params,
@@ -1,4 +1,4 @@
import { CreatePage } from "@/pages/(pst)/normal";
import { CreatePage } from "@/pages/(post)/normal";
export default async function PstBbsNormalAnswerPage({
params,
@@ -1,4 +1,4 @@
import { ModifyPage } from "@/pages/(pst)/normal";
import { ModifyPage } from "@/pages/(post)/normal";
export default async function PstBbsNormalPostModifyPage({
params,
@@ -1,4 +1,4 @@
import { NormalPost } from "@/pages/(pst)/normal";
import { NormalPost } from "@/pages/(post)/normal";
export default async function PstBbsNormalPostPage({
params,
@@ -1,4 +1,4 @@
import { NormalPage } from "@/pages/(pst)/normal";
import { NormalPage } from "@/pages/(post)/normal";
export default async function PstBbsNormalPage({
params,
@@ -1,4 +1,4 @@
import { CreatePage } from "@/pages/(pst)/normal";
import { CreatePage } from "@/pages/(post)/normal";
export default async function PstBbsNormalRegisterPage({
params,
@@ -1,4 +1,4 @@
import { ModifyPage } from "@/pages/(pst)/notice";
import { ModifyPage } from "@/pages/(post)/notice";
export default async function PstBbsNoticePostModifyPage({
params,
@@ -1,4 +1,4 @@
import { NoticePost } from "@/pages/(pst)/notice";
import { NoticePost } from "@/pages/(post)/notice";
export default async function PstBbsNoticePostPage({
params,
@@ -1,4 +1,4 @@
import { NoticePage } from "@/pages/(pst)/notice";
import { NoticePage } from "@/pages/(post)/notice";
export default async function PstBbsNoticePage({
params,
@@ -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");
+12 -1
View File
@@ -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;
+2 -1
View File
@@ -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,
@@ -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({
@@ -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({
@@ -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({
@@ -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({
@@ -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",
@@ -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">제목&uarr;</SelectItem>
<SelectItem value="pstTtl,desc">제목&darr;</SelectItem>
<SelectItem value="rgtrNm,asc">작성자&uarr;</SelectItem>
<SelectItem value="rgtrNm,desc">작성자&darr;</SelectItem>
<SelectItem value="regDt,asc">등록일&uarr;</SelectItem>
<SelectItem value="regDt,desc">등록일&darr;</SelectItem>
</SelectContent>
</Select>
);
}
export { SortSelect };
+25
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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