Merge remote-tracking branch 'origin/iipbs/sn/map' into iipbs/common/main
This commit is contained in:
@@ -67,10 +67,10 @@ npx create-next-app@latest ./apps/as --typescript --eslint --tailwind --app
|
||||
```json
|
||||
{
|
||||
// 기타 설정 ...
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@common/components/*": ["../../../packages/common/components/src/*"],
|
||||
"@common/components": ["../../../packages/common/components/src"],
|
||||
"@common/network/*": ["../../../packages/common/network/*"],
|
||||
"@common/components": ["../../../packages/common/components/dist/"],
|
||||
"@common/config": ["../../../packages/common/config/dist/"],
|
||||
"@common/network": ["../../../packages/common/network/"]
|
||||
}
|
||||
}
|
||||
@@ -95,51 +95,34 @@ export default {
|
||||
### 5. next.config.ts 수정
|
||||
```json
|
||||
import type { NextConfig } from "next";
|
||||
import webpack from "webpack";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL || "http://localhost:3000";
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
// common/components의 vite 환경 변수를 next 환경 변수로 정의
|
||||
new webpack.DefinePlugin({
|
||||
"import.meta.env.VITE_PORT": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PORT,
|
||||
),
|
||||
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST,
|
||||
),
|
||||
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL,
|
||||
),
|
||||
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
|
||||
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_CLAIM_NAME,
|
||||
),
|
||||
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_AUTH_USER_ID,
|
||||
),
|
||||
"process.env.SITE_ID": JSON.stringify(process.env.NEXT_PUBLIC_SITE_ID),
|
||||
}),
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/server/:path*",
|
||||
destination: `${serverApiUrl}/:path*`,
|
||||
},
|
||||
];
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -151,7 +134,7 @@ export default nextConfig;
|
||||
```css
|
||||
@import "@common/assets/styles/base.css";
|
||||
@import "@common/assets/styles/diveicon.css";
|
||||
@import "@common/assets/styles/components.css";
|
||||
@import "@common/components/index.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function WithBaseLayout({
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqPostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { FaqPage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <FaqPage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/faq";
|
||||
|
||||
export default async function PstBbsFaqRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/image";
|
||||
|
||||
export default async function PstBbsImagePostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ImagePost } from "@/pages/(pst)/image";
|
||||
|
||||
export default async function PstBbsImagePostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ImagePost bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ImagePage } from "@/pages/(pst)/image";
|
||||
|
||||
export default async function PstBbsImagePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <ImagePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/image";
|
||||
|
||||
export default async function PstBbsImageRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MoviePost } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <MoviePost bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { MoviePage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMoviePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <MoviePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/movie";
|
||||
|
||||
export default async function PstBbsMovieRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/normal";
|
||||
|
||||
export default async function PstBbsNormalAnswerPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <CreatePage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/normal";
|
||||
|
||||
export default async function PstBbsNormalPostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NormalPost } from "@/pages/(pst)/normal";
|
||||
|
||||
export default async function PstBbsNormalPostPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <NormalPost bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NormalPage } from "@/pages/(pst)/normal";
|
||||
|
||||
export default async function PstBbsNormalPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <NormalPage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/normal";
|
||||
|
||||
export default async function PstBbsNormalRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/(pst)/notice";
|
||||
|
||||
export default async function PstBbsNoticePostModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string; pstNo: string }>;
|
||||
}) {
|
||||
const { bbsId, pstNo } = await params;
|
||||
return <ModifyPage bbsId={bbsId} pstNo={pstNo} />;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NoticePost } from "@/pages/notice";
|
||||
import { NoticePost } from "@/pages/(pst)/notice";
|
||||
|
||||
export default async function PstBbsNoticePostPage({
|
||||
params,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NoticePage } from "@/pages/notice";
|
||||
import { NoticePage } from "@/pages/(pst)/notice";
|
||||
|
||||
export default async function PstBbsNoticePage({
|
||||
params,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CreatePage } from "@/pages/(pst)/notice";
|
||||
|
||||
export default async function PstBbsNoticeRegisterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <CreatePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "features",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 13000",
|
||||
@@ -13,12 +13,15 @@
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@common/network": "workspace:*",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"@minoru/react-dnd-treeview": "^3.5.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.67.1",
|
||||
"@tanstack/react-query-devtools": "^5.75.5",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tinymce/tinymce-react": "^6.1.0",
|
||||
"axios": "^1.8.1",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "./styles/base.css";
|
||||
import "./styles/components.css";
|
||||
import "@fontsource-variable/noto-sans-kr";
|
||||
|
||||
import { AppProvider } from "./providers";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { StoreProvider, PromptProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
import { PromptDialog } from "@/shared/ui/prompt-dialog";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { initializeNextClientConfig } from "@common/config";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
initializeNextClientConfig();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<ConfirmDialog />
|
||||
</ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
<PromptDialog />
|
||||
<ConfirmDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</PromptProvider>
|
||||
</StoreProvider>
|
||||
<div className="text-2xl">
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,8 +5,16 @@
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
@layer base {
|
||||
:root {
|
||||
--secondary-background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
[data-theme="black"] {
|
||||
--secondary-background: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
html,
|
||||
body {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
@@ -25,7 +26,7 @@ export class AttachmentApiService implements AttachmentService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteAttachment(atflId: string): Promise<void> {
|
||||
public async deleteAttachmentByAtflId(atflId: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ export class AttachmentApiService implements AttachmentService {
|
||||
);
|
||||
}
|
||||
|
||||
public async getImageUrl(atflId: string): Promise<string> {
|
||||
public async getImageUrlByAtflId(atflId: string): Promise<string> {
|
||||
if (!atflId) return "";
|
||||
const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`);
|
||||
const blob = await response.blob();
|
||||
@@ -50,6 +51,40 @@ export class AttachmentApiService implements AttachmentService {
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getStreamVideoUrlByAtflId(atflId: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO}/${atflId}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getMovieThumbnailUrlByAtflId(atflId: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/${atflId}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async getMovieThumbnailUrlByAtflCd(atflCd: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/atflCd/${atflCd}`,
|
||||
);
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
public async downloadAttachmentByAtflCd(
|
||||
atflCd: string,
|
||||
): Promise<AxiosResponse> {
|
||||
const response = await client.get(
|
||||
`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`,
|
||||
{ responseType: "blob" },
|
||||
);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export const AttachmentApi = new AttachmentApiService();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface AttachmentParams extends PageableParams {
|
||||
@@ -35,8 +36,12 @@ export type AttachmentList = PageableList<Attachment>;
|
||||
export interface AttachmentService {
|
||||
getAttachmentsList: (params: AttachmentParams) => Promise<AttachmentList>;
|
||||
getAttachmentsInfo: (atflCd: string) => Promise<Attachment[]>;
|
||||
deleteAttachment: (atflId: string) => Promise<void>;
|
||||
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
|
||||
deleteAttachmentByAtflCd: (atflCd: string) => Promise<void>;
|
||||
getImageUrl: (atflId: string) => Promise<string>;
|
||||
getImageUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
getStreamVideoUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getMovieThumbnailUrlByAtflId: (atflId: string) => Promise<string>;
|
||||
getMovieThumbnailUrlByAtflCd: (atflCd: string) => Promise<string>;
|
||||
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { LoginApi } from "./api/login-service";
|
||||
export { AuthWrapper } from "./lib/auth-wrapper";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const response = await client.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
);
|
||||
if (!userInfo) setUserInfo(response.data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { AuthWrapper };
|
||||
@@ -1,8 +1,10 @@
|
||||
export { BoardApi } from "./api/board-service";
|
||||
export {
|
||||
boardFormSchema,
|
||||
BOARD_POSTING_CODE,
|
||||
type Board,
|
||||
type BoardFormData,
|
||||
type BoardParams,
|
||||
type BoardPopup,
|
||||
type BoardPostingCode,
|
||||
} from "./model/board-interface";
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
import type { BbsType } from "@/shared/store/menu-store";
|
||||
|
||||
export const BOARD_TYPE = {
|
||||
BBSNORMAL: "BBSNORMAL",
|
||||
BBSFAQ: "BBSFAQ",
|
||||
BBSIMAGE: "BBSIMAGE",
|
||||
BBSMOVIE: "BBSMOVIE",
|
||||
export const BOARD_POSTING_CODE = {
|
||||
팝업게시: "01",
|
||||
상단게시: "02",
|
||||
기간게시: "03",
|
||||
} as const;
|
||||
|
||||
export type BoardType = (typeof BOARD_TYPE)[keyof typeof BOARD_TYPE];
|
||||
export type BoardPostingCode =
|
||||
(typeof BOARD_POSTING_CODE)[keyof typeof BOARD_POSTING_CODE];
|
||||
|
||||
export interface BoardParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
@@ -37,7 +38,7 @@ export interface BoardPopupParams extends PageableParams {
|
||||
|
||||
export interface Board {
|
||||
bbsId: number;
|
||||
bbsTypeCd: string;
|
||||
bbsTypeCd: BbsType;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
|
||||
@@ -11,9 +11,15 @@ import { CodeApi } from "../api/code-service";
|
||||
|
||||
type CodeSelectProps = {
|
||||
upCd: string;
|
||||
placeholder?: string;
|
||||
} & React.ComponentPropsWithoutRef<typeof Select>;
|
||||
|
||||
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
|
||||
function CodeSelect({
|
||||
upCd,
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
...props
|
||||
}: CodeSelectProps) {
|
||||
const { data: code } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
|
||||
queryFn: () => CodeApi.getCode(upCd),
|
||||
@@ -21,7 +27,7 @@ function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
|
||||
});
|
||||
|
||||
const defaultValueText =
|
||||
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? "선택";
|
||||
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
commentFormSchema,
|
||||
commentUpdateFormSchema,
|
||||
type Comment,
|
||||
type CommentService,
|
||||
type CommentList,
|
||||
type CommentParams,
|
||||
type CommentFormData,
|
||||
type CommentUpdateFormData,
|
||||
} from "../model/comment-interface";
|
||||
|
||||
export class CommentApiService implements CommentService {
|
||||
public async getComments(params: CommentParams): Promise<CommentList> {
|
||||
const response = await client.get<CommentList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createComment(data: CommentFormData): Promise<Comment> {
|
||||
const validatedData = commentFormSchema.parse(data);
|
||||
const response = await client.post<Comment>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateComment(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
cmntNo: number,
|
||||
data: CommentUpdateFormData,
|
||||
): Promise<Comment> {
|
||||
const validatedData = commentUpdateFormSchema.parse(data);
|
||||
const response = await client.put<Comment>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteComment(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
cmntNo: number,
|
||||
cmntPswd: string,
|
||||
): Promise<Comment> {
|
||||
const response = await client.delete<Comment>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`,
|
||||
{ params: { cmntPswd } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const CommentApi = new CommentApiService();
|
||||
@@ -0,0 +1,10 @@
|
||||
export { CommentApi } from "./api/comment-service";
|
||||
export {
|
||||
commentFormSchema,
|
||||
commentUpdateFormSchema,
|
||||
type Comment,
|
||||
type CommentList,
|
||||
type CommentParams,
|
||||
type CommentFormData,
|
||||
type CommentUpdateFormData,
|
||||
} from "./model/comment-interface";
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface CommentParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
pstNo: number;
|
||||
delYn?: string;
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
cmntEvl: number;
|
||||
pstNo: number;
|
||||
cmntNo: number;
|
||||
cmntCn: string;
|
||||
wrtrNm: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
delYn: string;
|
||||
}
|
||||
|
||||
export const commentFormSchema = z.object({
|
||||
pstNo: z.number(),
|
||||
cmntCn: z.string(),
|
||||
wrtrNm: z.string().nullish(),
|
||||
cmntPswd: z.string(),
|
||||
cmntEvl: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const commentUpdateFormSchema = commentFormSchema.omit({
|
||||
pstNo: true,
|
||||
});
|
||||
|
||||
export type CommentList = PageableList<Comment>;
|
||||
export type CommentFormData = z.infer<typeof commentFormSchema>;
|
||||
export type CommentUpdateFormData = z.infer<typeof commentUpdateFormSchema>;
|
||||
|
||||
export interface CommentService {
|
||||
getComments(params: CommentParams): Promise<CommentList>;
|
||||
createComment(data: CommentFormData): Promise<Comment>;
|
||||
updateComment(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
cmntNo: number,
|
||||
data: CommentUpdateFormData,
|
||||
): Promise<Comment>;
|
||||
deleteComment(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
cmntNo: number,
|
||||
cmntPswd: string,
|
||||
): Promise<Comment>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuAuthorityApi } from "../api/menu-authority-service";
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
MenuAuthorityParams,
|
||||
MenuAuthorityPayload,
|
||||
MenuAuthorityService,
|
||||
PageMenuAuthority,
|
||||
} from "../model/menu-authority-interface";
|
||||
|
||||
export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
@@ -18,6 +19,15 @@ export class MenuAuthorityApiService implements MenuAuthorityService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPageMenuAuthority(
|
||||
menuCd: number,
|
||||
): Promise<PageMenuAuthority[]> {
|
||||
const response = await client.get<PageMenuAuthority[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}/pageinfo/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`,
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { MenuAuthorityApi } from "./api/menu-authority-service";
|
||||
export { type PageMenuAuthority } from "./model/menu-authority-interface";
|
||||
export { ManagementAuthorityDialog } from "./ui/management-authority-dialog";
|
||||
|
||||
@@ -7,6 +7,14 @@ export interface MenuAuthorityParams extends PageableParams {
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface PageMenuAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export const menuAuthoritySchema = z.object({
|
||||
groupId: z.string(),
|
||||
groupNm: z.string(),
|
||||
@@ -35,5 +43,6 @@ export type MenuAuthorityFormData = z.infer<typeof menuAuthorityFormSchema>;
|
||||
|
||||
export interface MenuAuthorityService {
|
||||
getMenuAuthorityList(params: MenuAuthorityParams): Promise<MenuAuthorityList>;
|
||||
getPageMenuAuthority(menuCd: number): Promise<PageMenuAuthority[]>;
|
||||
saveMenuAuthority(data: MenuAuthorityPayload[]): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import {
|
||||
menuAuthorityFormSchema,
|
||||
type MenuAuthorityFormData,
|
||||
@@ -33,7 +33,7 @@ function ManagementAuthorityDialog({
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuAuthorityList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd],
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
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 { useMenuStore } from "@/shared/lib";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } =
|
||||
useMenuStore((state) => state);
|
||||
|
||||
const getCurrentMenuInfo = async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
return getCurrentMenu(menuRoles, pathname ?? "");
|
||||
};
|
||||
|
||||
const getPageMenuAuthority = async () => {
|
||||
const menuCd = getCookie("menu-code-key");
|
||||
if (!menuCd) return;
|
||||
const pageMenuAuthority = await MenuAuthorityApi.getPageMenuAuthority(
|
||||
parseInt(menuCd),
|
||||
);
|
||||
return pageMenuAuthority;
|
||||
};
|
||||
|
||||
const checkInquiryAuthority = () => {
|
||||
return pageMenuAuthority?.inqAuthrtYn === "Y";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
const currentMenu = await getCurrentMenuInfo();
|
||||
if (currentMenu) setCurrentMenuInfo(currentMenu);
|
||||
|
||||
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
|
||||
if (!currentMenu) return;
|
||||
|
||||
setCurrentMenuInfo(currentMenu);
|
||||
const pageMenuAuthority = await getPageMenuAuthority();
|
||||
if (pageMenuAuthority?.length)
|
||||
setPageMenuAuthority(pageMenuAuthority[0]!);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageMenuAuthority) return;
|
||||
const inquiryAuthority = checkInquiryAuthority();
|
||||
if (!inquiryAuthority)
|
||||
alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/"));
|
||||
}, [pageMenuAuthority]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,29 +1,67 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
PostInfo,
|
||||
PostList,
|
||||
PostParams,
|
||||
PostService,
|
||||
import {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type PostInfo,
|
||||
type PostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
} from "../model/post-interface";
|
||||
|
||||
export class PostApiService implements PostService {
|
||||
public async getPostList(params: PostParams): Promise<PostList> {
|
||||
const response = await client.get<PostList>(ENDPOINT.BOARD_SERVICE.POSTS, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`,
|
||||
public async getPostList(
|
||||
bbsId: number,
|
||||
params: PostParams,
|
||||
): Promise<PostList> {
|
||||
const response = await client.get<PostList>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deletePost(bbsId: string, pstNo: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`);
|
||||
public async getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo> {
|
||||
const response = await client.get<PostInfo>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createPost(data: PostFormData): Promise<Post> {
|
||||
const validatedData = postFormSchema.parse(data);
|
||||
const response = await client.post<Post>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updatePost(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post> {
|
||||
const validatedData = postUpdateFormSchema.parse(data);
|
||||
const response = await client.put<Post>(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
validatedData,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deletePost(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
pstPswd?: string,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`,
|
||||
{ params: { pstPswd } },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
export { PostApi } from "./api/post-service";
|
||||
export type {
|
||||
Post,
|
||||
PostList,
|
||||
PostParams,
|
||||
PostService,
|
||||
export {
|
||||
postFormSchema,
|
||||
postUpdateFormSchema,
|
||||
type Post,
|
||||
type PostList,
|
||||
type PostParams,
|
||||
type PostService,
|
||||
type PostFormData,
|
||||
type PostUpdateFormData,
|
||||
} from "./model/post-interface";
|
||||
export { PostPageGuard } from "./lib/post-page-guard";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { BoardApi } from "@/entities/board";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
|
||||
function PostPageGuard({
|
||||
bbsId,
|
||||
children,
|
||||
}: {
|
||||
bbsId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const setBbsInfo = useMenuStore((state) => state.setBbsInfo);
|
||||
|
||||
const { data: boardInfo, isLoading } = useQuery({
|
||||
queryKey: [bbsId],
|
||||
queryFn: () => BoardApi.getBoardInfo(bbsId),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!boardInfo) return;
|
||||
setBbsInfo(boardInfo);
|
||||
}, [boardInfo]);
|
||||
|
||||
if (!isLoading && pathname && boardInfo) {
|
||||
if (!pathname.includes(boardInfo.bbsTypeCd.toLowerCase())) {
|
||||
alert("잘못된 경로로 접속하였습니다.", router.back);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PostPageGuard };
|
||||
@@ -1,21 +1,22 @@
|
||||
import {
|
||||
type PageableList,
|
||||
type PageableParams,
|
||||
} from "@/shared/model/api-interface";
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface PostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
popupYn?: string;
|
||||
upndFixYn?: string;
|
||||
prdPstgYn?: string;
|
||||
siteId?: string;
|
||||
bbsTypeCd?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
rowNo: number;
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
pstCn: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
@@ -29,6 +30,7 @@ export interface Post {
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgtrNm: string;
|
||||
cmntCnt: number;
|
||||
}
|
||||
|
||||
export interface PostInfo extends Post {
|
||||
@@ -38,10 +40,48 @@ export interface PostInfo extends Post {
|
||||
mdfr: string;
|
||||
}
|
||||
|
||||
const postEtcSchema = z.object({
|
||||
bbsPstgTypeCd: z.string(),
|
||||
pstgPrdSeCd: z.string(),
|
||||
pstgBgngDt: z.string().nullish(),
|
||||
pstgEndDt: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const postFormSchema = z.object({
|
||||
bbsId: z.number(),
|
||||
pstTtl: z.string().nonempty(),
|
||||
pstCn: z.string().nullish(),
|
||||
rfrncUrl: z.string().nullish(),
|
||||
popupYn: z.string().nullish(),
|
||||
popupYnEtc: z.array(postEtcSchema).nullish(),
|
||||
upndFixYn: z.string().nullish(),
|
||||
upndFixYnEtc: z.array(postEtcSchema).nullish(),
|
||||
prdPstgYn: z.string().nullish(),
|
||||
prdPstgYnEtc: z.array(postEtcSchema).nullish(),
|
||||
pstPswd: z.string(),
|
||||
wrtrNm: z.string().nullish(),
|
||||
wrtrCplc: z.string().nullish(),
|
||||
atflCd: z.string().nullish(),
|
||||
parentPstNo: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const postUpdateFormSchema = postFormSchema.omit({
|
||||
bbsId: true,
|
||||
parentPstNo: true,
|
||||
});
|
||||
|
||||
export type PostList = PageableList<Post>;
|
||||
export type PostFormData = z.infer<typeof postFormSchema>;
|
||||
export type PostUpdateFormData = z.infer<typeof postUpdateFormSchema>;
|
||||
|
||||
export interface PostService {
|
||||
getPostList(params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: string, pstNo: string): Promise<PostInfo>;
|
||||
deletePost(bbsId: string, pstNo: string): Promise<void>;
|
||||
getPostList(bbsId: number, params: PostParams): Promise<PostList>;
|
||||
getPostInfo(bbsId: number, pstNo: number): Promise<PostInfo>;
|
||||
createPost(data: PostFormData): Promise<Post>;
|
||||
updatePost(
|
||||
bbsId: number,
|
||||
pstNo: number,
|
||||
data: PostUpdateFormData,
|
||||
): Promise<Post>;
|
||||
deletePost(bbsId: number, pstNo: number, pstPswd?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { CodeSelect } from "@/entities/code/@x/posting";
|
||||
import { SiteApi, type Site } from "@/entities/site/@x/posting";
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PostingApi } from "../api/posting-service";
|
||||
import {
|
||||
@@ -36,7 +37,6 @@ import {
|
||||
type PostingType,
|
||||
type PostPayload,
|
||||
} from "../model/posting-interface";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
type DateRange = {
|
||||
from: string;
|
||||
@@ -61,7 +61,7 @@ const POSTING_TYPE_LABEL = {
|
||||
|
||||
function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
const [rememberPeriodCode, setRememberPeriodCode] = useState<
|
||||
Record<string, string>
|
||||
@@ -96,11 +96,13 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
const isAllSelected =
|
||||
siteList && selectedRows.length === siteList.content.length;
|
||||
|
||||
const postingPeriod = (siteId: string) => {
|
||||
const periodCode = Number(rememberPeriodCode[siteId]);
|
||||
const postingPeriodFormat = (siteId: string) => {
|
||||
const periodCode = parseInt(rememberPeriodCode[siteId] ?? "");
|
||||
return postingPeriod(periodCode);
|
||||
};
|
||||
|
||||
const postingPeriod = (periodCode: number) => {
|
||||
if (periodCode === 99) return "상시";
|
||||
|
||||
return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs()
|
||||
.add(periodCode, "day")
|
||||
.format("YYYY-MM-DD")}`;
|
||||
@@ -175,7 +177,9 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
|
||||
const handlePeriodCodeChange = (siteId: string) => (value: string) => {
|
||||
const from = dayjs().format("YYYY-MM-DD 00:00:00");
|
||||
const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59");
|
||||
const to = dayjs()
|
||||
.add(parseInt(value), "day")
|
||||
.format("YYYY-MM-DD 23:59:59");
|
||||
|
||||
setRememberPeriodCode((prev) => ({
|
||||
...prev,
|
||||
@@ -272,19 +276,20 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) {
|
||||
onValueChange={handlePeriodCodeChange(site.siteId)}
|
||||
disabled={!selectedRows.includes(i)}
|
||||
/>
|
||||
{!!Number(rememberPeriodCode[site.siteId]) && (
|
||||
{!!parseInt(rememberPeriodCode[site.siteId] ?? "") && (
|
||||
<span
|
||||
className={cn(
|
||||
"text-lg text-label",
|
||||
!selectedRows.includes(i) && "text-muted",
|
||||
)}
|
||||
>
|
||||
게시 기간: <u>{postingPeriod(site.siteId)}</u>
|
||||
게시 기간: <u>{postingPeriodFormat(site.siteId)}</u>
|
||||
</span>
|
||||
)}
|
||||
{Number(rememberPeriodCode[site.siteId]) === 0 && (
|
||||
{parseInt(rememberPeriodCode[site.siteId] ?? "") ===
|
||||
0 && (
|
||||
<DateRangePicker
|
||||
handleDateChange={(date) =>
|
||||
onValueChange={(date) =>
|
||||
handlePeriodDateChange(site.siteId)(
|
||||
date as DateRange | undefined,
|
||||
)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { SiteApi } from "../api/site-service";
|
||||
export type { Site } from "../model/site-interface";
|
||||
export { SiteSelect } from "../ui/site-select";
|
||||
@@ -5,3 +5,5 @@ export {
|
||||
type Site,
|
||||
type SiteFormData,
|
||||
} from "./model/site-interface";
|
||||
|
||||
export { SiteSelect } from "./ui/site-select";
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { SiteApi } from "../api/site-service";
|
||||
|
||||
type SiteSelectProps = {
|
||||
siteId: string;
|
||||
placeholder?: string;
|
||||
} & React.ComponentPropsWithoutRef<typeof Select>;
|
||||
|
||||
function SiteSelect({
|
||||
siteId,
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
...props
|
||||
}: SiteSelectProps) {
|
||||
const { data: sites } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList(),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
const defaultValueText =
|
||||
sites?.content.find((item) => item.siteId === defaultValue)?.siteNm ?? placeholder;
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sites?.content.map((item) => (
|
||||
<SelectItem key={item.siteId} value={item.siteId}>
|
||||
{item.siteNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export { SiteSelect };
|
||||
@@ -0,0 +1 @@
|
||||
export { handleTusUpload } from "./lib/utils";
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Upload, type HttpResponse } from "tus-js-client";
|
||||
import { getCookie, setCookie } from "@/shared/lib";
|
||||
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT } from "@/shared/config";
|
||||
|
||||
type UploadOptions = {
|
||||
handleUploadStart?: (upload: Upload) => void;
|
||||
handleProgress?: (percentage: string) => void;
|
||||
handleSuccess?: (upload: Upload) => void;
|
||||
handleResponse?: (res: HttpResponse, upload: Upload) => void;
|
||||
};
|
||||
|
||||
export const handleTusUpload = (file: File, options?: UploadOptions) => {
|
||||
let metadata = {};
|
||||
|
||||
if (!file) return;
|
||||
metadata = {
|
||||
filename: file.name,
|
||||
filetype: file.type,
|
||||
};
|
||||
|
||||
const atflCd = getCookie("atfl-cd-key");
|
||||
if (atflCd) {
|
||||
metadata = {
|
||||
...metadata,
|
||||
atflCd,
|
||||
};
|
||||
}
|
||||
|
||||
const upload = new Upload(file, {
|
||||
endpoint: `${ENDPOINT.ATTACH_SERVICE.TUS_UPLOAD}`,
|
||||
chunkSize: 10 * 1024 * 1024,
|
||||
headers: {
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
metadata,
|
||||
onError(error) {
|
||||
console.error(`Failed because: ${error}`);
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
options?.handleProgress?.(percentage);
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
setCookie("atfl-cd-key", JSON.parse(fileInfo)?.atflCd ?? "");
|
||||
options?.handleResponse?.(res, upload);
|
||||
},
|
||||
onSuccess() {
|
||||
options?.handleSuccess?.(upload);
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
options?.handleUploadStart?.(upload);
|
||||
|
||||
console.log("upload start >>>>>", upload);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { FaqPage } from "./ui/faq-page";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,316 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { PostApi, type PostParams } from "@/entities/post";
|
||||
import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import dayjs from "dayjs";
|
||||
import React from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
function FaqPage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state);
|
||||
|
||||
const [postParams, setPostParams] = useState<PostParams>({
|
||||
keywordType: "title",
|
||||
keyword: "",
|
||||
});
|
||||
const [selectedPostNumbers, setSelectedPostNumbers] = useState<number[]>([]);
|
||||
|
||||
const {
|
||||
data: postList,
|
||||
isFetched,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsInfo?.bbsTypeCd],
|
||||
queryFn: () =>
|
||||
PostApi.getPostList(parseInt(bbsId), {
|
||||
...postParams,
|
||||
bbsTypeCd: bbsInfo?.bbsTypeCd,
|
||||
}),
|
||||
enabled: !!bbsInfo?.bbsTypeCd,
|
||||
});
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: ({ pstNo, pstPswd }: { pstNo: number; pstPswd?: string }) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, pstPswd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeletePost = async (pstNo: number) => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
deletePost({ pstNo, pstPswd: userInfo?.userUnqId });
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedPostDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await Promise.all(
|
||||
selectedPostNumbers.map((pstNo) =>
|
||||
PostApi.deletePost(parseInt(bbsId), pstNo, userInfo?.userUnqId),
|
||||
),
|
||||
);
|
||||
refetch();
|
||||
setSelectedPostNumbers([]);
|
||||
alert("삭제되었습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (pstNo: number, checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true
|
||||
? [...selectedPostNumbers, pstNo]
|
||||
: selectedPostNumbers.filter((num) => num !== pstNo),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAllCheckboxChange = (checked: boolean | string) => {
|
||||
setSelectedPostNumbers(
|
||||
checked === true ? postList?.content.map((item) => item.pstNo) || [] : [],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<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 });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[10rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="title">제목</SelectItem>
|
||||
<SelectItem value="all">제목+내용</SelectItem>
|
||||
<SelectItem value="writer">작성자</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
value={postParams.keyword}
|
||||
onChange={(e) =>
|
||||
setPostParams({ ...postParams, keyword: e.target.value })
|
||||
}
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
{pageMenuAuthority?.regAuthrtYn === "Y" && (
|
||||
<Button onClick={() => router.push(`${pathname}/register`)}>
|
||||
등록
|
||||
</Button>
|
||||
)}
|
||||
{pageMenuAuthority?.delAuthrtYn === "Y" && (
|
||||
<Button
|
||||
color="red"
|
||||
onClick={onSelectedPostDelete}
|
||||
disabled={!selectedPostNumbers.length}
|
||||
>
|
||||
선택삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<div className="mb-4 flex items-center gap-2 rounded-sm border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={selectedPostNumbers.length === postList?.content.length}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={handleAllCheckboxChange}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="secondary flex flex-col gap-4"
|
||||
>
|
||||
{postList?.content.map((item) => (
|
||||
<div key={item.pstNo} className="flex items-start gap-2">
|
||||
<Checkbox
|
||||
id={item.pstNo.toString()}
|
||||
checked={selectedPostNumbers.includes(item.pstNo)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
handleCheckboxChange(item.pstNo, checked)
|
||||
}
|
||||
className="mx-2 my-6"
|
||||
/>
|
||||
<AccordionItem
|
||||
value={item.pstNo.toString()}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-primary">Q</span>
|
||||
<span>{item.pstTtl}</span>
|
||||
</div>
|
||||
<span className="px-4 text-[1.2rem] font-normal text-label">
|
||||
등록일: {dayjs(item.regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
A
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<pre className="whitespace-pre-wrap font-noto text-[1.4rem] tracking-tighter text-foreground">
|
||||
{item.pstCn}
|
||||
</pre>
|
||||
<Attachment atflCd={item.atflCd} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() =>
|
||||
router.push(`${pathname}/${item.pstNo}`)
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeletePost(item.pstNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
) : (
|
||||
<Skeleton className="h-[36rem] w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Attachment({ atflCd }: { atflCd: string }) {
|
||||
const [filename, setFilename] = useState("");
|
||||
|
||||
const { data: downloadResponse, isFetching } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd],
|
||||
queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
const blob = new Blob([downloadResponse.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename || "download";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadResponse) return;
|
||||
try {
|
||||
let filename = "";
|
||||
const contentDisposition =
|
||||
downloadResponse.headers["content-disposition"];
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename\*=UTF-8''([^;]+)/,
|
||||
);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
setFilename(filename);
|
||||
} catch {
|
||||
setFilename("");
|
||||
}
|
||||
}, [downloadResponse]);
|
||||
|
||||
return isFetching ? (
|
||||
<div className="my-8">
|
||||
<Skeleton className="h-[2.5rem] w-[32rem]" />
|
||||
</div>
|
||||
) : (
|
||||
filename && (
|
||||
<div className="my-8 flex items-center gap-4">
|
||||
<span className="font-bold text-label">첨부파일</span>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
className="flex w-fit items-center gap-2"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<i className="diveicon di-download" />
|
||||
<span>{filename}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { FaqPage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">FAQ 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ImagePage } from "./ui/image-page";
|
||||
export { ImagePost } from "./ui/image-post";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { PostPage as ImagePage } from "@/widgets/post";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { Post as ImagePost } from "@/widgets/post";
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { MoviePage } from "./ui/movie-page";
|
||||
export { MoviePost } from "./ui/movie-post";
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">동영상 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">동영상 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { PostPage as MoviePage } from "@/widgets/post";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { Post as MoviePost } from "@/widgets/post";
|
||||
@@ -0,0 +1,4 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { NormalPage } from "./ui/normal-page";
|
||||
export { NormalPost } from "./ui/normal-post";
|
||||
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId, pstNo }: { bbsId: string; pstNo?: string }) {
|
||||
const answerRegisterMode = !!pstNo;
|
||||
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
popupYn: "N",
|
||||
upndFixYn: "N",
|
||||
prdPstgYn: "N",
|
||||
parentPstNo: pstNo ? parseInt(pstNo) : null,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{answerRegisterMode ? "답글 등록" : "게시글 등록"}
|
||||
</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { PostPage as NormalPage } from "@/widgets/post";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { Post as NormalPost } from "@/widgets/post";
|
||||
+2
@@ -1,2 +1,4 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { NoticePage } from "./ui/notice-page";
|
||||
export { NoticePost } from "./ui/notice-post";
|
||||
+35
-6
@@ -1,13 +1,13 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { Post } from "@/entities/post";
|
||||
import { Badge, Button } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post } from "@/entities/post";
|
||||
|
||||
export const columns: ColumnDef<Post>[] = [
|
||||
{
|
||||
id: "no",
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
@@ -51,11 +51,40 @@ export const columns: ColumnDef<Post>[] = [
|
||||
];
|
||||
|
||||
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
|
||||
if (!atflCd) return null;
|
||||
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
let filename = "";
|
||||
const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd);
|
||||
const contentDisposition = response.headers["content-disposition"];
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename\*=UTF-8''([^;]+)/,
|
||||
);
|
||||
if (filenameMatch) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button size="sm" color="white" className="flex items-center gap-2">
|
||||
<Button size="icon" color="white" onClick={handleDownload}>
|
||||
<i className="diveicon di-download" />
|
||||
다운로드
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import { PostApi, postFormSchema, type PostFormData } from "@/entities/post";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function CreatePage({ bbsId }: { bbsId: string }) {
|
||||
const router = useRouter();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { mutateAsync: createPost } = useMutation({
|
||||
mutationFn: (data: PostFormData) => PostApi.createPost(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostFormData>({
|
||||
resolver: zodResolver(postFormSchema),
|
||||
values: {
|
||||
bbsId: parseInt(bbsId),
|
||||
pstTtl: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
popupYn: "N",
|
||||
upndFixYn: "N",
|
||||
prdPstgYn: "N",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">공지 게시글 등록</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={createPost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { PostForm } from "@/widgets/post";
|
||||
import {
|
||||
PostApi,
|
||||
postUpdateFormSchema,
|
||||
type PostUpdateFormData,
|
||||
} from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const { data: postInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo],
|
||||
queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)),
|
||||
});
|
||||
|
||||
const { mutateAsync: updatePost } = useMutation({
|
||||
mutationFn: (data: PostUpdateFormData) =>
|
||||
PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<PostUpdateFormData>({
|
||||
resolver: zodResolver(postUpdateFormSchema),
|
||||
values: {
|
||||
...postInfo,
|
||||
pstTtl: postInfo?.pstTtl ?? "",
|
||||
pstPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">공지 게시글 수정</h3>
|
||||
</div>
|
||||
<PostForm form={form} handleSave={updatePost} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { PostPage as NoticePage } from "@/widgets/post";
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { Post as NoticePost } from "@/widgets/post";
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Board } from "@/entities/board";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { useLocalStorage } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
function NoticeLayout({ children }: { children: React.ReactNode }) {
|
||||
const [bbsInfo, setBbsInfo] = useState<Board | null>(null);
|
||||
const [storedBbsInfo] = useLocalStorage("bbs-info", "");
|
||||
|
||||
useEffect(() => {
|
||||
if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo));
|
||||
}, [storedBbsInfo]);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
title={bbsInfo?.bbsNm ?? ""}
|
||||
menuPaths={["서비스관리", "게시판관리"]}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { NoticeLayout };
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Post } from "@/widgets/post";
|
||||
import { NoticeLayout } from "./notice-layout";
|
||||
|
||||
function NoticePost({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
|
||||
return (
|
||||
<NoticeLayout>
|
||||
<Post bbsId={bbsId} pstNo={pstNo} />
|
||||
</NoticeLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export { NoticePost };
|
||||
@@ -7,7 +7,10 @@ export const ENDPOINT = {
|
||||
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
|
||||
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
|
||||
IMAGES: "/attach-service/api/v1/images",
|
||||
MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail",
|
||||
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
|
||||
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
|
||||
STREAM_VIDEO: "/attach-service/api/v1/stream/video",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
@@ -36,7 +39,8 @@ export const ENDPOINT = {
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
POSTS: "/board-service/api/v1/posts",
|
||||
POSTS_USER: "/board-service/api/v1/posts/user",
|
||||
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { useAlertStore } from "./use-alert-store";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
export { usePrompt } from "./use-prompt";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useContext } from "react";
|
||||
import { PromptContext } from "../providers/prompt-provider";
|
||||
|
||||
export const usePrompt = () => {
|
||||
const { message, resolve, setMessage, setResolve } =
|
||||
useContext(PromptContext);
|
||||
|
||||
const prompt = (message: string) => {
|
||||
setMessage(message);
|
||||
return new Promise<{ resolved: boolean; value?: string }>((resolve) => {
|
||||
setResolve(() => resolve);
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (value: string) => {
|
||||
resolve?.({ resolved: true, value });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
resolve?.({ resolved: false });
|
||||
setMessage("");
|
||||
};
|
||||
|
||||
return { message, isOpen: !!message, prompt, onSubmit, onCancel };
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const useTus = () => {
|
||||
},
|
||||
onProgress(bytesUploaded, bytesTotal) {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
setProgress(Number(percentage));
|
||||
setProgress(parseInt(percentage));
|
||||
},
|
||||
onAfterResponse(req, res) {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { StoreProvider } from "./store-provider";
|
||||
export { PromptProvider } from "./prompt-provider";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
message?: string;
|
||||
resolve?: ({
|
||||
resolved,
|
||||
value,
|
||||
}: {
|
||||
resolved: boolean;
|
||||
value?: string;
|
||||
}) => void;
|
||||
setMessage: (message: string) => void;
|
||||
setResolve: (resolve: () => void) => void;
|
||||
}
|
||||
|
||||
export const PromptContext = createContext<PromptContext>({
|
||||
message: "",
|
||||
resolve() {},
|
||||
setMessage() {},
|
||||
setResolve() {},
|
||||
});
|
||||
|
||||
export const PromptProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [resolve, setResolve] =
|
||||
useState<
|
||||
({ resolved, value }: { resolved: boolean; value?: string }) => void
|
||||
>();
|
||||
|
||||
return (
|
||||
<PromptContext.Provider
|
||||
value={{ message, resolve, setMessage, setResolve }}
|
||||
>
|
||||
{children}
|
||||
</PromptContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,93 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
export const BBS_TYPE = {
|
||||
BBSNOTICE: "BBSNOTICE",
|
||||
BBSNORMAL: "BBSNORMAL",
|
||||
BBSFAQ: "BBSFAQ",
|
||||
BBSIMAGE: "BBSIMAGE",
|
||||
BBSMOVIE: "BBSMOVIE",
|
||||
} as const;
|
||||
|
||||
export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE];
|
||||
|
||||
export interface IBbs {
|
||||
bbsId: number;
|
||||
bbsTypeCd: BbsType;
|
||||
bbsTypeNm: string;
|
||||
bbsNm: string;
|
||||
bbsExpln: string;
|
||||
rfrncUseYn: string;
|
||||
atflUseYn: string;
|
||||
atflMaxCnt: number;
|
||||
atflMaxFileSz: number;
|
||||
ansUseYn: string;
|
||||
ansUseAuthrt: string;
|
||||
cmntUseYn: string;
|
||||
dgstfnEvlUseYn: string;
|
||||
smsUseYn: string;
|
||||
upndFixUseYn: string;
|
||||
pstgPrdUseYn: string;
|
||||
popupUseYn: string;
|
||||
delYn: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
interface IMenu {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: IMenu[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
interface IAuthority {
|
||||
mngAuthrtYn: string;
|
||||
inqAuthrtYn: string;
|
||||
regAuthrtYn: string;
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
}
|
||||
|
||||
export type MenuState = {
|
||||
currentMenuInfo: MenuItem | null;
|
||||
bbsInfo: IBbs | null;
|
||||
currentMenuInfo: IMenu | null;
|
||||
pageMenuAuthority: IAuthority | null;
|
||||
};
|
||||
|
||||
export type MenuActions = {
|
||||
setCurrentMenuInfo: (menu: MenuItem) => void;
|
||||
setBbsInfo: (bbs: IBbs) => void;
|
||||
setCurrentMenuInfo: (menu: IMenu) => void;
|
||||
setPageMenuAuthority: (authority: IAuthority) => void;
|
||||
};
|
||||
|
||||
export type MenuStore = MenuState & MenuActions;
|
||||
|
||||
export const defaultInitState: MenuState = {
|
||||
currentMenuInfo: null,
|
||||
bbsInfo: null,
|
||||
pageMenuAuthority: null,
|
||||
};
|
||||
|
||||
export const createMenuStore = (initState: MenuState = defaultInitState) => {
|
||||
return createStore<MenuStore>()((set) => ({
|
||||
...initState,
|
||||
setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }),
|
||||
setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }),
|
||||
setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }),
|
||||
setPageMenuAuthority: (authority: IAuthority) =>
|
||||
set({ pageMenuAuthority: authority }),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface IUserInfo {
|
||||
mdfcnDt: string;
|
||||
rgtr: string;
|
||||
mdfr: string;
|
||||
adminRoleYn: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Button,
|
||||
} from "@common/components";
|
||||
import { useAlertStore } from "../lib/use-alert-store";
|
||||
|
||||
function AlertDialog() {
|
||||
const { message, callback, setMessage } = useAlertStore((state) => state);
|
||||
const handleOpenChange = () => {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { type FieldValues, type UseFormReturn } from "react-hook-form";
|
||||
import { Button } from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { Form } from "./form";
|
||||
|
||||
type FormWrapperProps<T extends FieldValues> = {
|
||||
form: UseFormReturn<T>;
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { HTMLAttributes } from "react";
|
||||
import {
|
||||
flexRender,
|
||||
type RowData,
|
||||
type Table as TableInstance,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button, Checkbox, Label, cn } from "@common/components";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
align?: "left" | "center" | "right";
|
||||
rowSpan?: number;
|
||||
}
|
||||
}
|
||||
|
||||
type GridViewDataTableProps<T> = {
|
||||
table: TableInstance<T>;
|
||||
variant?: "default" | "secondary" | "line";
|
||||
className?: HTMLAttributes<HTMLTableElement>["className"];
|
||||
viewPagination?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
function GridViewDataTable<T>({
|
||||
table,
|
||||
className,
|
||||
viewPagination,
|
||||
onRowClick,
|
||||
}: GridViewDataTableProps<T>) {
|
||||
return (
|
||||
<div className="bg-secondary-background pb-10">
|
||||
<div className="flex items-center gap-2 rounded-sm rounded-bl-none rounded-br-none border border-border bg-background p-4">
|
||||
<Checkbox
|
||||
id="all-select"
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="all-select" className="text-2xl">
|
||||
전체선택
|
||||
</Label>
|
||||
</div>
|
||||
<div className={cn("w-full p-4", className)}>
|
||||
<div className="grid grid-cols-5 gap-6">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-md border border-border bg-background p-4 shadow-md",
|
||||
onRowClick && "cursor-pointer hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const { meta } = cell.column.columnDef;
|
||||
return (
|
||||
<div key={cell.id} {...meta}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{viewPagination && <Pagination table={table} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Pagination<T>({ table }: { table: TableInstance<T> }) {
|
||||
const pageCount = table.getPageCount();
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const maxVisiblePages = 10; // 한 번에 보여줄 최대 페이지 수
|
||||
|
||||
let startPage = Math.max(0, currentPage - maxVisiblePages / 2); // 시작 페이지
|
||||
const endPage = Math.min(pageCount, startPage + maxVisiblePages); // 끝 페이지
|
||||
|
||||
if (endPage - startPage < maxVisiblePages) {
|
||||
startPage = Math.max(0, endPage - maxVisiblePages);
|
||||
}
|
||||
|
||||
const pageNumbers = Array(endPage - startPage)
|
||||
.fill(0)
|
||||
.map((_, i) => startPage + i);
|
||||
|
||||
return (
|
||||
<div className="mt-[24px] flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-10 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">첫 페이지로</span>
|
||||
<DoubleArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-10 p-0"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className="sr-only">이전 페이지로</span>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{pageNumbers.map((pageNumber) => (
|
||||
<Button
|
||||
key={pageNumber}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"inline-block size-10 !bg-transparent p-0 text-foreground hover:!bg-accent",
|
||||
currentPage === pageNumber &&
|
||||
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
|
||||
)}
|
||||
onClick={() => table.setPageIndex(pageNumber)}
|
||||
>
|
||||
{pageNumber + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-10 p-0"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">다음 페이지로</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-10 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className="sr-only">마지막 페이지로</span>
|
||||
<DoubleArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { GridViewDataTable };
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { usePrompt } from "@/shared/lib/use-prompt";
|
||||
|
||||
function PromptDialog() {
|
||||
const { message, isOpen, onSubmit, onCancel } = usePrompt();
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onCancel}>
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<div className="flex flex-col gap-6 pb-4 pt-8">
|
||||
<p className="whitespace-pre-line text-center text-[1.4rem] text-foreground">
|
||||
{message}
|
||||
</p>
|
||||
<Input
|
||||
type="password"
|
||||
value={value}
|
||||
className="!text-[1.3rem]"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button onClick={() => onSubmit(value)}>확인</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={onCancel} color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PromptDialog };
|
||||
@@ -1 +0,0 @@
|
||||
export { FormWrapper } from "./ui/form-wrapper";
|
||||
@@ -1 +1,3 @@
|
||||
export { Post } from "./ui/post";
|
||||
export { PostForm } from "./ui/post-form";
|
||||
export { PostPage } from "./ui/post-page";
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Checkbox, Skeleton, cn } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post } from "@/entities/post";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { BbsType } from "@/shared/store/menu-store";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface TableMeta<TData> {
|
||||
bbsTypeCd?: BbsType;
|
||||
}
|
||||
}
|
||||
|
||||
export const createColumns = (
|
||||
thumbRefs: React.MutableRefObject<Map<number, string>>,
|
||||
): ColumnDef<Post>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row, table }) => {
|
||||
const { atflCd, pstNo, pstTtl, inqNocs, rgtrNm, regDt } = row.original;
|
||||
const bbsTypeCd = table.options.meta?.bbsTypeCd;
|
||||
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
|
||||
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (bbsTypeCd === "BBSMOVIE") {
|
||||
return (
|
||||
<div className="-m-4">
|
||||
<ThumbImage
|
||||
atflCd={atflCd}
|
||||
pstNo={pstNo}
|
||||
thumbRefs={thumbRefs}
|
||||
bbsTypeCd={bbsTypeCd}
|
||||
classes={{
|
||||
image: "object-cover",
|
||||
}}
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
|
||||
{dayjs().diff(dayjs(regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
<span className="text-[1.6rem] font-bold text-label">
|
||||
{pstTtl}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{rgtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(regDt).format("YYYY-MM-DD")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type ThumbImageProps = {
|
||||
atflCd: string;
|
||||
pstNo: number;
|
||||
thumbRefs: React.MutableRefObject<Map<number, string>>;
|
||||
bbsTypeCd?: string;
|
||||
classes?: {
|
||||
image?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ThumbImage = ({
|
||||
atflCd,
|
||||
pstNo,
|
||||
thumbRefs,
|
||||
bbsTypeCd,
|
||||
classes,
|
||||
}: ThumbImageProps) => {
|
||||
const prevImage = thumbRefs.current.get(pstNo);
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => {
|
||||
if (bbsTypeCd === "BBSIMAGE") {
|
||||
return AttachmentApi.getImageUrlByAtflCd(atflCd);
|
||||
} else if (bbsTypeCd === "BBSMOVIE") {
|
||||
return AttachmentApi.getMovieThumbnailUrlByAtflCd(atflCd);
|
||||
}
|
||||
},
|
||||
select: (url) => {
|
||||
if (url) thumbRefs.current.set(pstNo, url);
|
||||
return url;
|
||||
},
|
||||
enabled: !prevImage && !!atflCd,
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="h-[16rem] w-full">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (imageUrl)
|
||||
return (
|
||||
<div className="relative flex h-[16rem] w-full items-center justify-center">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className={cn("object-contain", classes?.image)}
|
||||
fill
|
||||
alt="썸네일 이미지"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-[16rem] w-full items-center justify-center rounded-sm border border-border bg-background">
|
||||
<i className="diveicon di-image text-3xl" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Button, Checkbox } from "@common/components";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post } from "@/entities/post";
|
||||
|
||||
export const columns: ColumnDef<Post & { isUpndFixPost?: boolean }>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "rowNo",
|
||||
header: "NO",
|
||||
cell: ({ row }) => {
|
||||
if (row.original.isUpndFixPost) {
|
||||
return (
|
||||
<Badge
|
||||
color="orange"
|
||||
className="flex size-8 items-center justify-center"
|
||||
>
|
||||
<i className="diveicon di-volume-up-fill" />
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return row.original.rowNo;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "pstTtl",
|
||||
header: "제목",
|
||||
cell: ({ row }) => {
|
||||
const { ansPstn, pstTtl, inqNocs, cmntCnt } = row.original;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex min-w-[20vw] items-center gap-2 text-left"
|
||||
style={{ paddingInlineStart: `${(ansPstn - 1) * 20}px` }}
|
||||
>
|
||||
{ansPstn > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="-rotate-90">
|
||||
<i className="diveicon di-arrow-90left-down rotate-90" />
|
||||
</div>
|
||||
<Badge color="smokyBlue">답변</Badge>
|
||||
</div>
|
||||
)}
|
||||
{inqNocs >= 100 && <Badge color="red">HOT</Badge>}
|
||||
{dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && (
|
||||
<Badge color="blue">NEW</Badge>
|
||||
)}
|
||||
{pstTtl}
|
||||
{cmntCnt > 0 && (
|
||||
<div className="ml-4 flex items-end gap-2">
|
||||
<i className="diveicon di-chat-box-left-text text-gray-400" />
|
||||
<span className="text-gray-400">{cmntCnt}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inqNocs",
|
||||
header: "조회수",
|
||||
},
|
||||
{
|
||||
accessorKey: "atflCd",
|
||||
header: "첨부파일",
|
||||
cell: ({ row }) => {
|
||||
const atflCd = row.original.atflCd;
|
||||
if (!atflCd) return null;
|
||||
return <AttachmentButton atflCd={atflCd} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "rgtrNm",
|
||||
header: "작성자",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일",
|
||||
cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"),
|
||||
},
|
||||
];
|
||||
|
||||
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
|
||||
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
let filename = "";
|
||||
const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd);
|
||||
const contentDisposition = response.headers["content-disposition"];
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(
|
||||
/filename\*=UTF-8''([^;]+)/,
|
||||
);
|
||||
if (filenameMatch) {
|
||||
filename = decodeURIComponent(filenameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button size="icon" color="white" onClick={handleDownload}>
|
||||
<i className="diveicon di-download" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { FieldPath, UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { Button, FileUploader } from "@common/components";
|
||||
import { handleTusUpload } from "@/entities/tus";
|
||||
import { type Attachment } from "@/entities/attachment";
|
||||
import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
|
||||
function GeneralFileUploader<T extends { atflCd?: string | null }>({
|
||||
form,
|
||||
attachmentData,
|
||||
deleteAttachmentIds,
|
||||
setDeleteAttachmentIds,
|
||||
}: {
|
||||
form: UseFormReturn<T>;
|
||||
attachmentData?: Attachment[];
|
||||
deleteAttachmentIds: string[];
|
||||
setDeleteAttachmentIds: (deleteAttachmentIds: string[]) => void;
|
||||
}) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
|
||||
const [attachments, setAttachments] = useState<Record<string, Attachment>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const handleTusResponse = (res: HttpResponse, upload: Upload) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (
|
||||
typeof fileInfo !== "string" ||
|
||||
fileInfo === undefined ||
|
||||
fileInfo === null
|
||||
)
|
||||
return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd" as FieldPath<T>, atflCd);
|
||||
if (atflId) {
|
||||
setAttachments((prev) => ({
|
||||
...prev,
|
||||
[atflId]: {
|
||||
atflCd,
|
||||
atflId,
|
||||
orgnlFileNm: (upload.file as File).name,
|
||||
atflFlieSz: (upload.file as File).size,
|
||||
atflType: (upload.file as File).type,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const onUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const atflCd = form.getValues("atflCd" as FieldPath<T>);
|
||||
if (atflCd) setCookie("atfl-cd-key", atflCd as string);
|
||||
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
if (bbsInfo?.atflMaxCnt && files.length > bbsInfo.atflMaxCnt) {
|
||||
alert("첨부파일 최대 개수를 초과했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const fileSize = Math.round(file.size / 1024); // Byte -> KB
|
||||
if (bbsInfo?.atflMaxFileSz && fileSize > bbsInfo.atflMaxFileSz) {
|
||||
alert("첨부파일 최대 크기를 초과했습니다.");
|
||||
return;
|
||||
}
|
||||
handleTusUpload(file, {
|
||||
handleResponse: handleTusResponse,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const mapper = (data: Attachment[]) =>
|
||||
data.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.atflId] = curr;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Attachment>,
|
||||
);
|
||||
if (attachmentData) setAttachments(mapper(attachmentData));
|
||||
}, [attachmentData]);
|
||||
|
||||
return (
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
multiple
|
||||
className="flex !h-auto max-w-full flex-row-reverse items-start gap-4 [&>label]:h-[3.2rem]"
|
||||
onUpload={onUpload}
|
||||
customPreview={
|
||||
<div className="flex w-full flex-wrap gap-4">
|
||||
{Object.entries(attachments).map(([atflId, attachment]) => {
|
||||
if (deleteAttachmentIds.includes(atflId)) return null;
|
||||
return (
|
||||
<div
|
||||
key={atflId}
|
||||
className="flex h-[3.2rem] items-center gap-2 rounded-sm border border-border bg-background px-3 py-2 text-foreground"
|
||||
>
|
||||
<span>{attachment.orgnlFileNm}</span>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex size-7 items-center justify-center rounded-full p-0"
|
||||
onClick={() =>
|
||||
setDeleteAttachmentIds([...deleteAttachmentIds, atflId])
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-x text-xs" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { GeneralFileUploader };
|
||||
@@ -1,32 +1,41 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { PostApi } from "@/entities/post";
|
||||
import { useMenuStore, useUserStore } from "@/shared/lib";
|
||||
|
||||
type PostActionProps = {
|
||||
bbsId: string;
|
||||
pstNo: string;
|
||||
children: React.ReactNode;
|
||||
handleDelete: () => Promise<void>;
|
||||
};
|
||||
|
||||
function PostAction({ children, bbsId, pstNo }: PostActionProps) {
|
||||
function PostAction({ children, handleDelete }: PostActionProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const { mutate: deletePost } = useMutation({
|
||||
mutationFn: () => PostApi.deletePost(bbsId, pstNo),
|
||||
onSuccess: () => {
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) deletePost();
|
||||
if (await confirm("삭제하시겠습니까?")) handleDelete();
|
||||
};
|
||||
|
||||
const checkAnsUseAuthrt = () => {
|
||||
// 관리자
|
||||
if (bbsInfo?.ansUseAuthrt === "A") {
|
||||
return userInfo?.adminRoleYn === "Y";
|
||||
}
|
||||
// 회원
|
||||
if (bbsInfo?.ansUseAuthrt === "M") {
|
||||
return !!userInfo;
|
||||
}
|
||||
// 비회원
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{children}
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
@@ -37,14 +46,29 @@ function PostAction({ children, bbsId, pstNo }: PostActionProps) {
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => router.push(`${pathname}/modify`)}
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
{bbsInfo?.ansUseYn === "Y" && (
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={() => router.push(`${pathname}/answer`)}
|
||||
disabled={!checkAnsUseAuthrt()}
|
||||
>
|
||||
답글
|
||||
</Button>
|
||||
)}
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Input, Textarea } from "@common/components";
|
||||
import {
|
||||
CommentApi,
|
||||
commentFormSchema,
|
||||
type CommentFormData,
|
||||
type CommentUpdateFormData,
|
||||
} from "@/entities/comment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, usePrompt, useUserStore } from "@/shared/lib";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) {
|
||||
const { prompt } = usePrompt();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const [editingCmntNo, setEditingCmntNo] = useState<number | null>(null);
|
||||
const [editValue, setEditValue] = useState<string>("");
|
||||
|
||||
const { data: comments } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS, pstNo],
|
||||
queryFn: () => CommentApi.getComments({ pstNo }),
|
||||
enabled: !!pstNo,
|
||||
});
|
||||
|
||||
const { mutate: createComment } = useMutation({
|
||||
mutationFn: (data: CommentFormData) => CommentApi.createComment(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.");
|
||||
form.setValue("cmntCn", "");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateComment } = useMutation({
|
||||
mutationFn: ({
|
||||
cmntNo,
|
||||
data,
|
||||
}: {
|
||||
cmntNo: number;
|
||||
data: CommentUpdateFormData;
|
||||
}) => CommentApi.updateComment(bbsId, pstNo, cmntNo, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
setEditingCmntNo(null);
|
||||
setEditValue("");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteComment } = useMutation({
|
||||
mutationFn: ({ cmntNo, cmntPswd }: { cmntNo: number; cmntPswd: string }) =>
|
||||
CommentApi.deleteComment(bbsId, pstNo, cmntNo, cmntPswd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<CommentFormData>({
|
||||
resolver: zodResolver(commentFormSchema),
|
||||
values: {
|
||||
pstNo,
|
||||
cmntCn: "",
|
||||
wrtrNm: userInfo?.userName ?? "",
|
||||
cmntPswd: userInfo?.userUnqId ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: CommentFormData) => {
|
||||
createComment(data);
|
||||
};
|
||||
|
||||
const getCommentPassword = async () => {
|
||||
let cmntPswd = "";
|
||||
if (userInfo?.userUnqId) {
|
||||
cmntPswd = userInfo?.userUnqId;
|
||||
} else {
|
||||
const { resolved, value } = await prompt("비밀번호를 입력해주세요.");
|
||||
if (resolved) cmntPswd = value ?? "";
|
||||
}
|
||||
return cmntPswd;
|
||||
};
|
||||
|
||||
const handleUpdateComment = async (cmntNo: number, wrtrNm: string) => {
|
||||
if (!(await confirm("수정하시겠습니까?"))) return;
|
||||
const cmntPswd = await getCommentPassword();
|
||||
updateComment({
|
||||
cmntNo,
|
||||
data: {
|
||||
cmntCn: editValue,
|
||||
wrtrNm,
|
||||
cmntPswd,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteComment = async (cmntNo: number) => {
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
const cmntPswd = await getCommentPassword();
|
||||
deleteComment({ cmntNo, cmntPswd });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card card-border !p-8">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cmntCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
maxLength={1000}
|
||||
className="h-[10rem] bg-background p-4 !text-[1.3rem] !leading-6"
|
||||
placeholder="댓글을 입력해주세요."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex gap-4">
|
||||
{!userInfo?.userName && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wrtrNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[16rem] bg-background !text-[1.3rem]"
|
||||
placeholder="작성자"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!userInfo?.userUnqId && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cmntPswd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] bg-background !text-[1.3rem]"
|
||||
placeholder="비밀번호"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit">등록</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<div className="mt-8 flex flex-col">
|
||||
{comments?.content.map((comment) => {
|
||||
if (comment.delYn === "Y") return null;
|
||||
return (
|
||||
<div
|
||||
className="flex justify-between gap-2 border-t border-gray-200 py-8"
|
||||
key={comment.cmntNo}
|
||||
>
|
||||
<div className="flex w-full gap-10">
|
||||
<div className="flex w-[10rem] shrink-0 flex-col">
|
||||
<span className="text-[1.4rem] font-bold text-label">
|
||||
{comment.wrtrNm}
|
||||
</span>
|
||||
<span className="text-[1.2rem] text-label">
|
||||
{dayjs(comment.regDt).format("YYYY.MM.DD")}
|
||||
</span>
|
||||
</div>
|
||||
{editingCmntNo === comment.cmntNo ? (
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Textarea
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
maxLength={1000}
|
||||
className="h-[10rem] bg-background p-4 !text-[1.3rem] !leading-6"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleUpdateComment(comment.cmntNo, comment.wrtrNm)
|
||||
}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
setEditingCmntNo(null);
|
||||
setEditValue("");
|
||||
}}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="break-all text-[1.4rem] text-foreground">
|
||||
{comment.cmntCn}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 whitespace-nowrap">
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => {
|
||||
setEditingCmntNo(comment.cmntNo);
|
||||
setEditValue(comment.cmntCn);
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={() => handleDeleteComment(comment.cmntNo)}
|
||||
>
|
||||
<i className="diveicon di-trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { PostComment };
|
||||
@@ -0,0 +1,452 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn, FieldPath } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
Button,
|
||||
} from "@common/components";
|
||||
import { BOARD_POSTING_CODE } from "@/entities/board";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useUserStore, setCookie, useMenuStore } from "@/shared/lib";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { PostingPeriodSelector } from "./posting-period-selector";
|
||||
import { GeneralFileUploader } from "./general-file-uploader";
|
||||
import { WithPreviewFileUploader } from "./with-preview-file-uploader";
|
||||
|
||||
type PostFormProps<T extends PostFormData | PostUpdateFormData> = {
|
||||
form: UseFormReturn<T>;
|
||||
handleSave: (data: T) => Promise<Post>;
|
||||
};
|
||||
|
||||
function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
form,
|
||||
handleSave,
|
||||
}: PostFormProps<T>) {
|
||||
const atflCd = form.getValues("atflCd" as FieldPath<T>);
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const bbsInfo = useMenuStore((state) => state.bbsInfo);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
|
||||
const [deleteAttachmentIds, setDeleteAttachmentIds] = useState<string[]>([]);
|
||||
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd as string),
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: T) => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
if (deleteAttachmentIds.length > 0) {
|
||||
await Promise.all(
|
||||
deleteAttachmentIds.map(AttachmentApi.deleteAttachmentByAtflId),
|
||||
);
|
||||
}
|
||||
await handleSave(data);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCookie("atfl-cd-key", "");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>제목</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstTtl" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={(field.value as string) ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="제목을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>작성자</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"wrtrNm" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={(field.value as string) ?? ""}
|
||||
className="w-[20rem] !text-[1.3rem]"
|
||||
placeholder="작성자를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{!userInfo?.userUnqId && (
|
||||
<TableRow>
|
||||
<TableHead>비밀번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstPswd" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
value={(field.value as string) ?? ""}
|
||||
className="w-[20rem] !text-[1.3rem]"
|
||||
placeholder="비밀번호를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstCn" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={(field.value as string) ?? ""}
|
||||
onChange={field.onChange}
|
||||
className="h-[20rem] w-full bg-form !text-[1.3rem] !leading-6"
|
||||
placeholder="내용을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{bbsInfo?.rfrncUseYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>참조 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"rfrncUrl" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={(field.value as string) ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="URL을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{bbsInfo?.popupUseYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>팝업여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"popupYn" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
name="popupYn"
|
||||
value={field.value?.toString() ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="popupY" />
|
||||
<Label htmlFor="popupY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="popupN" />
|
||||
<Label htmlFor="popupN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"popupYnEtc" as FieldPath<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues = form.getValues().popupYnEtc?.[0];
|
||||
return (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"popupYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
date,
|
||||
) => {
|
||||
field.onChange([
|
||||
{
|
||||
...fieldValues,
|
||||
...date,
|
||||
bbsPstgTypeCd:
|
||||
BOARD_POSTING_CODE.팝업게시,
|
||||
pstgPrdSeCd: periodCode,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
handlePostingDateChange={(date) => {
|
||||
field.onChange([
|
||||
{ ...fieldValues, ...date },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{bbsInfo?.upndFixUseYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>상단고정</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"upndFixYn" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
name="upndFixYn"
|
||||
value={field.value?.toString() ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="upndFixY" />
|
||||
<Label htmlFor="upndFixY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="upndFixN" />
|
||||
<Label htmlFor="upndFixN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"upndFixYnEtc" as FieldPath<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues =
|
||||
form.getValues().upndFixYnEtc?.[0];
|
||||
return (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"upndFixYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
date,
|
||||
) => {
|
||||
field.onChange([
|
||||
{
|
||||
...fieldValues,
|
||||
...date,
|
||||
bbsPstgTypeCd:
|
||||
BOARD_POSTING_CODE.상단게시,
|
||||
pstgPrdSeCd: periodCode,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
handlePostingDateChange={(date) => {
|
||||
field.onChange([
|
||||
{ ...fieldValues, ...date },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{bbsInfo?.pstgPrdUseYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>게시기간</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"prdPstgYn" as FieldPath<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
name="prdPstgYn"
|
||||
value={field.value?.toString() ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="prdPstgY" />
|
||||
<Label htmlFor="prdPstgY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="prdPstgN" />
|
||||
<Label htmlFor="prdPstgN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"prdPstgYnEtc" as FieldPath<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues =
|
||||
form.getValues().prdPstgYnEtc?.[0];
|
||||
return (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"prdPstgYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
date,
|
||||
) => {
|
||||
field.onChange([
|
||||
{
|
||||
...fieldValues,
|
||||
...date,
|
||||
bbsPstgTypeCd:
|
||||
BOARD_POSTING_CODE.기간게시,
|
||||
pstgPrdSeCd: periodCode,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
handlePostingDateChange={(date) => {
|
||||
field.onChange([
|
||||
{ ...fieldValues, ...date },
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{bbsInfo?.atflUseYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
{bbsInfo?.bbsTypeCd === "BBSIMAGE" ? (
|
||||
<WithPreviewFileUploader
|
||||
form={form}
|
||||
attachmentData={attachmentData}
|
||||
/>
|
||||
) : (
|
||||
<GeneralFileUploader
|
||||
form={form}
|
||||
attachmentData={attachmentData}
|
||||
deleteAttachmentIds={deleteAttachmentIds}
|
||||
setDeleteAttachmentIds={setDeleteAttachmentIds}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export { PostForm };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user