feat: 공지게시판 목록 작업
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
import { BannerPage } from "@/pages/banner";
|
||||
export default BannerPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/banner";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/banner";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { BbsPage } from "@/pages/bbs";
|
||||
export default BbsPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/bbs";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage bbsId={id} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/bbs";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { ContentsPage } from "@/pages/contents";
|
||||
export default ContentsPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/contents";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/contents";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { DashboardPage } from "@/pages/dashboard";
|
||||
export default DashboardPage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { FilePage } from "@/pages/file";
|
||||
export default FilePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { ImagePage } from "@/pages/image";
|
||||
export default ImagePage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/image";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/image";
|
||||
export default CreatePage;
|
||||
@@ -5,7 +5,6 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
import { BaseLayout } from "@common/components/ui";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
@@ -29,7 +28,7 @@ export default function WithBaseLayout({
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
{children}
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { MenuPage } from "@/pages/menu";
|
||||
export default MenuPage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { ModulePage } from "@/pages/module";
|
||||
export default ModulePage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/module";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/module";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { OrganizationPage } from "@/pages/organization";
|
||||
export default OrganizationPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NoticePage } from "@/pages/notice";
|
||||
|
||||
export default async function PstBbsNoticePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ bbsId: string }>;
|
||||
}) {
|
||||
const { bbsId } = await params;
|
||||
return <NoticePage bbsId={bbsId} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { SitePage } from "@/pages/site";
|
||||
export default SitePage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/site";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
return <VModifyPage siteId={id} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/site";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { TemplatePage } from "@/pages/template";
|
||||
export default TemplatePage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/template";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/template";
|
||||
export default CreatePage;
|
||||
@@ -1,2 +0,0 @@
|
||||
import { TermsPage } from "@/pages/terms";
|
||||
export default TermsPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/terms";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ sequential: string }>;
|
||||
}) {
|
||||
const { sequential } = await params;
|
||||
return <VModifyPage sequential={sequential} />;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { CreatePage } from "@/pages/terms";
|
||||
export default CreatePage;
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
const serverApiUrl =
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000";
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:13000";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
|
||||
@@ -1,32 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "x-rewrite-me",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/board/:path*",
|
||||
destination: `${subDomain}/board/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev -p 13000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p 13000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
PostList,
|
||||
PostParams,
|
||||
PostService,
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
export const PostApi = new PostApiService();
|
||||
@@ -0,0 +1,7 @@
|
||||
export { PostApi } from "./api/post-service";
|
||||
export type {
|
||||
Post,
|
||||
PostList,
|
||||
PostParams,
|
||||
PostService,
|
||||
} from "./model/post-interface";
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
type PageableList,
|
||||
type PageableParams,
|
||||
} from "@/shared/model/api-interface";
|
||||
|
||||
export interface PostParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
bbsId?: number;
|
||||
delYn?: string;
|
||||
frDateYmd?: string;
|
||||
toDateYmd?: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
bbsId: number;
|
||||
pstNo: number;
|
||||
pstTtl: string;
|
||||
inqNocs: number;
|
||||
rfrncUrl: string;
|
||||
popupYn: string;
|
||||
upndFixYn: string;
|
||||
prdPstgYn: string;
|
||||
rootPath: string;
|
||||
pstPath: string;
|
||||
ansPstn: number;
|
||||
wrtrNm: string;
|
||||
wrtrCplc: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgtrNm: string;
|
||||
}
|
||||
|
||||
export type PostList = PageableList<Post>;
|
||||
|
||||
export interface PostService {
|
||||
getPostList(params: PostParams): Promise<PostList>;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
BannerFormData,
|
||||
BannerInfo,
|
||||
BannerList,
|
||||
BannerParams,
|
||||
BannerService,
|
||||
} from "../model/banner-interface";
|
||||
|
||||
export class BannerApiService implements BannerService {
|
||||
public async getBannerList(params: BannerParams): Promise<BannerList> {
|
||||
const response = await client.get<BannerList>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getBannerInfo(bnrSn: string): Promise<BannerInfo> {
|
||||
const response = await client.get<BannerInfo>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createBanner(data: BannerFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
ENDPOINT.CMS_SERVICE.BANNERS,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateBanner(
|
||||
bnrSn: string,
|
||||
data: BannerFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteBanner(bnrSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const BannerApi = new BannerApiService();
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { BannerPage } from "./ui/banner-page";
|
||||
@@ -1,61 +0,0 @@
|
||||
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) => void;
|
||||
};
|
||||
|
||||
export const handleUpload = (files: FileList, options?: UploadOptions) => {
|
||||
let metadata = {};
|
||||
|
||||
const file = files.item(0);
|
||||
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);
|
||||
},
|
||||
onSuccess() {
|
||||
options?.handleSuccess?.(upload);
|
||||
},
|
||||
});
|
||||
|
||||
upload.start();
|
||||
options?.handleUploadStart?.(upload);
|
||||
|
||||
console.log("upload start >>>>>", upload);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const BANNER_TYPE = {
|
||||
BNRMAIN: "BNRMAIN",
|
||||
BNRFTER: "BNRFTER",
|
||||
} as const;
|
||||
|
||||
export type BannerType = (typeof BANNER_TYPE)[keyof typeof BANNER_TYPE];
|
||||
|
||||
export interface BannerParams extends PageableParams {
|
||||
bnrTypeCd?: BannerType;
|
||||
bnrTtl?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Banner {
|
||||
bnrSn: number;
|
||||
bnrTypeCd: BannerType;
|
||||
bnrTypeNm: string;
|
||||
bnrTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
useYn: string;
|
||||
atflCd: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export interface BannerInfo extends Omit<Banner, "mdfr"> {
|
||||
bnrExpln: string;
|
||||
newWndwYn: string;
|
||||
atflCd: string;
|
||||
regDt: string;
|
||||
rgTrNm: string;
|
||||
}
|
||||
|
||||
export const bannerFormSchema = z.object({
|
||||
bnrTypeCd: z.string(),
|
||||
bnrTtl: z.string(),
|
||||
bnrExpln: z.string().nullish(),
|
||||
lnkgUrlAddr: z.string().nullish(),
|
||||
newWndwYn: z.string(),
|
||||
atflCd: z.string().nullish(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type BannerFormData = z.infer<typeof bannerFormSchema>;
|
||||
export type BannerList = PageableList<Banner>;
|
||||
|
||||
export interface BannerService {
|
||||
getBannerList: (params: BannerParams) => Promise<BannerList>;
|
||||
getBannerInfo: (bnrSn: string) => Promise<BannerInfo>;
|
||||
createBanner: (data: BannerFormData) => Promise<void>;
|
||||
updateBanner: (bnrSn: string, data: BannerFormData) => Promise<void>;
|
||||
deleteBanner: (bnrSn: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Image from "next/image";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox, Skeleton } from "@common/components/ui";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { Banner } from "./banner-interface";
|
||||
|
||||
export const createColumns = (
|
||||
imageRefs: React.MutableRefObject<Map<number, string>>,
|
||||
): ColumnDef<Banner>[] => [
|
||||
{
|
||||
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"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "배너 이미지",
|
||||
cell: ({ row }) => (
|
||||
<BannerImage
|
||||
atflCd={row.original.atflCd}
|
||||
bnrSn={row.original.bnrSn}
|
||||
imageRefs={imageRefs}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "bnrTtl",
|
||||
header: "배너명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bnrTypeNm",
|
||||
header: "배너구분",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type BannerImageProps = {
|
||||
atflCd: string;
|
||||
bnrSn: number;
|
||||
imageRefs: React.MutableRefObject<Map<number, string>>;
|
||||
};
|
||||
|
||||
const BannerImage = ({ atflCd, bnrSn, imageRefs }: BannerImageProps) => {
|
||||
const prevImage = imageRefs.current.get(bnrSn);
|
||||
|
||||
const { data: imageUrl, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd],
|
||||
queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd),
|
||||
select: (url) => {
|
||||
if (url) imageRefs.current.set(bnrSn, url);
|
||||
return url;
|
||||
},
|
||||
enabled: !prevImage && !!atflCd,
|
||||
});
|
||||
|
||||
if (isLoading) return <Skeleton className="h-[4rem] w-[16rem]" />;
|
||||
|
||||
if (!imageUrl) return null;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-[4rem] w-[16rem] items-center justify-center">
|
||||
<Image src={imageUrl} className="object-contain" fill alt="배너 이미지" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: 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 }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,296 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import {
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { setCookie } from "@/shared/lib/cookie";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type { BannerFormData } from "../model/banner-interface";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
|
||||
type BannerFormProps = {
|
||||
form: UseFormReturn<BannerFormData>;
|
||||
bnrSn?: number;
|
||||
handleSave: (data: BannerFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
|
||||
const atflCd = form.watch("atflCd");
|
||||
|
||||
const [imageUrl, setImageUrl] = useState("");
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd!),
|
||||
select: (data) => data?.[data.length - 1],
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
|
||||
if (attachmentData)
|
||||
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!attachmentData) return;
|
||||
|
||||
(async () => {
|
||||
setImageUrl(await AttachmentApi.getImageUrl(attachmentData.atflId));
|
||||
})();
|
||||
}, [attachmentData]);
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!bnrSn} {...props}>
|
||||
<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="bnrTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="BNRTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 이미지</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
setCookie("atfl-cd-key", "");
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<div className="relative h-full w-full">
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt="업로드된 이미지"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>
|
||||
이미지 파일(jpg, jpeg, png, gif)만 업로드
|
||||
가능합니다.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="배너에 연결할 링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>링크 열기</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newWndwYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="newWndwYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="newWndwN" />
|
||||
<Label htmlFor="newWndwN">현재창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="newWndwY" />
|
||||
<Label htmlFor="newWndwY">새창</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>배너 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bnrExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem] !leading-6"
|
||||
placeholder="배너에 대한 설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerForm };
|
||||
@@ -1,144 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { createColumns } from "../model/banner-table-columns";
|
||||
import { type Banner, type BannerParams } from "../model/banner-interface";
|
||||
import { PreviewBannerDialog } from "./preview-banner-dialog";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function BannerPage() {
|
||||
const router = useRouter();
|
||||
const imageRefs = useRef<Map<number, string>>(new Map());
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [bannerQuery, setBannerQuery] = useState<BannerParams>({ bnrTtl: "" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: bannerList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS],
|
||||
queryFn: () =>
|
||||
BannerApi.getBannerList({
|
||||
...bannerQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: bannerList?.content ?? [],
|
||||
columns: createColumns(imageRefs),
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: bannerList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Banner) => {
|
||||
router.push(`/banner/register/${row.bnrSn}`);
|
||||
};
|
||||
|
||||
const getSelectedBannerImages = () => {
|
||||
const selectedImages: string[] = [];
|
||||
table.getFilteredSelectedRowModel().rows.forEach((row) => {
|
||||
const imageUrl = imageRefs.current.get(row.original.bnrSn);
|
||||
if (imageUrl) selectedImages.push(imageUrl);
|
||||
});
|
||||
return selectedImages;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.bnrTtl,
|
||||
pstgTrgtNo: row.original.bnrSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
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 htmlFor="" className="mr-2 text-label">
|
||||
배너명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={bannerQuery.bnrTtl}
|
||||
onChange={(e) =>
|
||||
setBannerQuery({ ...bannerQuery, bnrTtl: e.target.value })
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<PreviewBannerDialog images={getSelectedBannerImages()} />
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.BANNER}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/banner/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BannerPage };
|
||||
@@ -1,46 +0,0 @@
|
||||
"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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) => BannerApi.createBanner(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
defaultValues: {
|
||||
newWndwYn: "N",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 등록</h3>
|
||||
</div>
|
||||
<BannerForm form={form} handleSave={createBanner} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
bannerFormSchema,
|
||||
type BannerFormData,
|
||||
} from "../model/banner-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { BannerApi } from "../api/banner-service";
|
||||
import { BannerForm } from "./banner-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: bannerDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.BANNERS, sequential],
|
||||
queryFn: () => BannerApi.getBannerInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateBanner } = useMutation({
|
||||
mutationFn: (data: BannerFormData) =>
|
||||
BannerApi.updateBanner(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteBanner } = useMutation({
|
||||
mutationFn: () => BannerApi.deleteBanner(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
});
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerFormSchema),
|
||||
values: bannerDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
savePosting(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">배너 수정</h3>
|
||||
</div>
|
||||
<BannerForm
|
||||
form={form}
|
||||
bnrSn={Number(sequential)}
|
||||
handleSave={updateBanner}
|
||||
handleDelete={deleteBanner}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 게시 이력</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Button,
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "@common/components";
|
||||
|
||||
type PreviewBannerDialogProps = {
|
||||
images: string[];
|
||||
};
|
||||
|
||||
function PreviewBannerDialog({ images }: PreviewBannerDialogProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="white" disabled={!images.length}>
|
||||
<i className="diveicon di-display" />
|
||||
미리보기
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>배너 이미지 미리보기</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 px-12">
|
||||
<Carousel>
|
||||
<CarouselContent>
|
||||
{images.map((image, i) => (
|
||||
<CarouselItem key={`banner-${i}`} className="basis-1/3">
|
||||
<div className="relative flex aspect-video items-center justify-center">
|
||||
<Image
|
||||
src={image}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt={`배너 이미지 ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
))}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious className="!bg-accent text-accent-foreground" />
|
||||
<CarouselNext className="!bg-accent text-accent-foreground" />
|
||||
</Carousel>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { PreviewBannerDialog };
|
||||
@@ -1,3 +0,0 @@
|
||||
export { BbsPage } from "./ui/bbs-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
@@ -1,41 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components/ui";
|
||||
import type { Board } from "@/entities/board/model/board-interface";
|
||||
|
||||
export const columns: ColumnDef<Board>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsTypeNm",
|
||||
header: "게시판유형",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) =>
|
||||
row.original.mdfcnDt
|
||||
? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
cell: () => <Button size="sm">관리</Button>,
|
||||
},
|
||||
];
|
||||
@@ -1,447 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { Board, BoardFormData } from "@/entities/board";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
|
||||
type BbsFormProps = {
|
||||
form: UseFormReturn<BoardFormData>;
|
||||
bbsId?: string;
|
||||
handleSave: (data: BoardFormData) => Promise<Board>;
|
||||
};
|
||||
|
||||
function BbsForm({ form, bbsId, ...props }: BbsFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!bbsId} {...props}>
|
||||
<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="bbsNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="게시판명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시판 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bbsExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="게시판설명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시판 유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bbsTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="BBSTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>참조 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rfrncUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="rfrncUseY" />
|
||||
<Label htmlFor="rfrncUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="rfrncUseN" />
|
||||
<Label htmlFor="rfrncUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflUseY" />
|
||||
<Label htmlFor="atflUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflUseN" />
|
||||
<Label htmlFor="atflUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 최대 개수</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflMaxCnt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(value === "" ? null : Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="최대 개수 입력"
|
||||
disabled={form.watch("atflUseYn") === "N"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일 최대 크기</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflMaxFileSz"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(value === "" ? null : Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="최대 크기 입력"
|
||||
disabled={form.watch("atflUseYn") === "N"}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>답글 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ansUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="ansUseY" />
|
||||
<Label htmlFor="ansUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="ansUseN" />
|
||||
<Label htmlFor="ansUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>답글 사용 권한</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ansUseAuthrt"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={form.watch("ansUseYn") === "N"}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="A">관리자</SelectItem>
|
||||
<SelectItem value="M">회원</SelectItem>
|
||||
<SelectItem value="N">일반</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>댓글 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cmntUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="cmntUseY" />
|
||||
<Label htmlFor="cmntUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="cmntUseN" />
|
||||
<Label htmlFor="cmntUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시물평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dgstfnEvlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="dgstfnEvlUseY" />
|
||||
<Label htmlFor="dgstfnEvlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="dgstfnEvlUseN" />
|
||||
<Label htmlFor="dgstfnEvlUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>팝업 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="popupUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="popupUseY" />
|
||||
<Label htmlFor="popupUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="popupUseN" />
|
||||
<Label htmlFor="popupUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>SMS알림 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smsUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="smsUseY" />
|
||||
<Label htmlFor="smsUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="smsUseN" />
|
||||
<Label htmlFor="smsUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>상단고정 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="upndFixUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="upndFixUseY" />
|
||||
<Label htmlFor="upndFixUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="upndFixUseN" />
|
||||
<Label htmlFor="upndFixUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>게시기간 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pstgPrdUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="pstgPrdUseY" />
|
||||
<Label htmlFor="pstgPrdUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="pstgPrdUseN" />
|
||||
<Label htmlFor="pstgPrdUseN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup {...field} onValueChange={field.onChange}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { BbsForm };
|
||||
@@ -1,145 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { BoardApi, type BoardParams } from "@/entities/board";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { type Board } from "@/entities/board";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function BbsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [boardParams, setBoardParams] = useState<BoardParams>({
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () =>
|
||||
BoardApi.getBoardList({
|
||||
...boardParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: boardList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: boardList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Board) => {
|
||||
router.push(`/bbs/register/${row.bbsId}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
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 htmlFor="bbsType" className="mr-2 text-label">
|
||||
게시판 유형
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="BBSTYPE"
|
||||
value={boardParams.bbsTypeCd}
|
||||
onValueChange={(value) =>
|
||||
setBoardParams({ ...boardParams, bbsTypeCd: value })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="bbsNm" className="ml-4 mr-2 text-label">
|
||||
게시판명
|
||||
</Label>
|
||||
<Input
|
||||
id="bbsNm"
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={boardParams.bbsNm}
|
||||
onChange={(e) =>
|
||||
setBoardParams({ ...boardParams, bbsNm: e.target.value })
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<Button onClick={() => router.push("/bbs/register")}>등록</Button>
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-[3.2rem] w-[12rem]">
|
||||
<SelectValue placeholder="페이지 크기" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
{pageSize}개씩 보기
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BbsPage };
|
||||
@@ -1,55 +0,0 @@
|
||||
"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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
BoardApi,
|
||||
boardFormSchema,
|
||||
type BoardFormData,
|
||||
} from "@/entities/board";
|
||||
import { BbsForm } from "./bbs-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createBoard } = useMutation({
|
||||
mutationFn: (data: BoardFormData) => BoardApi.createBoard(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BoardFormData>({
|
||||
resolver: zodResolver(boardFormSchema),
|
||||
defaultValues: {
|
||||
bbsTypeCd: "",
|
||||
bbsNm: "",
|
||||
bbsExpln: "",
|
||||
rfrncUseYn: "Y",
|
||||
atflUseYn: "Y",
|
||||
ansUseYn: "Y",
|
||||
cmntUseYn: "Y",
|
||||
dgstfnEvlUseYn: "Y",
|
||||
smsUseYn: "Y",
|
||||
upndFixUseYn: "Y",
|
||||
pstgPrdUseYn: "Y",
|
||||
popupUseYn: "Y",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시판 등록</h3>
|
||||
</div>
|
||||
<BbsForm form={form} handleSave={createBoard} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,45 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
BoardApi,
|
||||
boardFormSchema,
|
||||
type BoardFormData,
|
||||
} from "@/entities/board";
|
||||
import { BbsForm } from "./bbs-form";
|
||||
|
||||
function ModifyPage({ bbsId }: { bbsId: string }) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: boardInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS, bbsId],
|
||||
queryFn: () => BoardApi.getBoardInfo(bbsId),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateBoard } = useMutation({
|
||||
mutationFn: (data: BoardFormData) => BoardApi.updateBoard(bbsId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<BoardFormData>({
|
||||
resolver: zodResolver(boardFormSchema),
|
||||
values: boardInfo,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">게시판 수정</h3>
|
||||
</div>
|
||||
<BbsForm form={form} bbsId={bbsId} handleSave={updateBoard} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,3 +0,0 @@
|
||||
export { ContentsPage } from "./ui/contents-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
@@ -1,68 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import type { Contents } from "@/entities/content";
|
||||
|
||||
const columnHelper = createColumnHelper<Contents>();
|
||||
|
||||
export const columns = [
|
||||
columnHelper.display({
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
}),
|
||||
columnHelper.accessor("contsTtl", {
|
||||
id: "contsTtl",
|
||||
header: "콘텐츠명",
|
||||
}),
|
||||
columnHelper.group({
|
||||
id: "workInfo",
|
||||
header: "작성정보",
|
||||
columns: [
|
||||
columnHelper.accessor("workContsVer", {
|
||||
header: "작성버전",
|
||||
cell: ({ row }) =>
|
||||
row.original.workContsVer ? `rev. ${row.original.workContsVer}` : "",
|
||||
}),
|
||||
columnHelper.accessor("workAprvNm", {
|
||||
header: "작성상태",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
columnHelper.group({
|
||||
id: "aprvInfo",
|
||||
header: "승인정보",
|
||||
columns: [
|
||||
columnHelper.accessor("aprvContsVer", {
|
||||
header: "승인버전",
|
||||
cell: ({ row }) =>
|
||||
row.original.aprvContsVer ? `rev. ${row.original.aprvContsVer}` : "",
|
||||
}),
|
||||
columnHelper.accessor("autzrNm", {
|
||||
header: "승인자",
|
||||
}),
|
||||
columnHelper.accessor("aprvDt", {
|
||||
header: "승인일자",
|
||||
cell: ({ row }) =>
|
||||
row.original.aprvDt
|
||||
? dayjs(row.original.aprvDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
columnHelper.accessor("useYn", {
|
||||
id: "useYn",
|
||||
header: "사용여부",
|
||||
}),
|
||||
columnHelper.accessor("mdfrNm", {
|
||||
id: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
}),
|
||||
columnHelper.accessor("mdfcnDt", {
|
||||
id: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) =>
|
||||
row.original.mdfcnDt
|
||||
? dayjs(row.original.mdfcnDt).format("YYYY-MM-DD")
|
||||
: "",
|
||||
}),
|
||||
];
|
||||
@@ -1,268 +0,0 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { ContentsFormData } from "@/entities/content";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import Image from "next/image";
|
||||
|
||||
const Editor = dynamic(
|
||||
() => import("@/entities/editor").then((module) => module.Editor),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface ContentsFormProps {
|
||||
form: UseFormReturn<ContentsFormData>;
|
||||
contsSn?: number;
|
||||
isEditable?: boolean;
|
||||
isContsTtlDisabled?: boolean;
|
||||
handleSave: (data: ContentsFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
}
|
||||
|
||||
function ContentsForm({
|
||||
form,
|
||||
contsSn,
|
||||
isEditable,
|
||||
isContsTtlDisabled,
|
||||
...props
|
||||
}: ContentsFormProps) {
|
||||
const { data: cclCode } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"],
|
||||
queryFn: () => CodeApi.getCode("CCLCD"),
|
||||
});
|
||||
|
||||
const { data: koglCode } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"],
|
||||
queryFn: () => CodeApi.getCode("KOGLCD"),
|
||||
});
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!contsSn} {...props}>
|
||||
<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="contsTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="콘텐츠 제목을 입력하십시오."
|
||||
disabled={!isEditable || isContsTtlDisabled}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="contsCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Editor
|
||||
value={field.value ?? ""}
|
||||
onEditorChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>CCL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cclCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="cclCd"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="" id="ccl-none" />
|
||||
<Label
|
||||
htmlFor="ccl-none"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
해당없음
|
||||
</Label>
|
||||
</div>
|
||||
{cclCode?.map((code) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={code.lwpoCd}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={code.lwpoCd}
|
||||
id={`ccl-${code.lwpoCd}`}
|
||||
/>
|
||||
<Label htmlFor={`ccl-${code.lwpoCd}`}>
|
||||
<Image
|
||||
src={`/images/${code.unit}`}
|
||||
alt="CCL"
|
||||
width={77}
|
||||
height={27}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>공공누리</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="koglCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="koglCd"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="" id="koglNone" />
|
||||
<Label
|
||||
htmlFor="koglNone"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
해당없음
|
||||
</Label>
|
||||
</div>
|
||||
{koglCode?.map((code) => (
|
||||
<div
|
||||
className="flex items-center space-x-2"
|
||||
key={code.lwpoCd}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={code.lwpoCd}
|
||||
id={`kogl-${code.lwpoCd}`}
|
||||
/>
|
||||
<Label htmlFor={`kogl-${code.lwpoCd}`}>
|
||||
<div className="relative h-[2.7rem] w-[11rem]">
|
||||
<Image
|
||||
src={`/images/${code.unit}`}
|
||||
alt="공공누리"
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>태그</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tag"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="태그를 입력하십시오."
|
||||
disabled={!isEditable}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentsForm };
|
||||
@@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import {
|
||||
ContentsApi,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
} from "@/entities/content";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function ContentsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [contentsQuery, setContentsQuery] = useState<ContentsParams>({
|
||||
contsTtl: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: contentsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsList({
|
||||
...contentsQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Contents) => {
|
||||
router.push(`/contents/register/${row.contsSn}`);
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: contentsList?.content ?? [],
|
||||
columns,
|
||||
rowCount: contentsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
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 htmlFor="" className="mr-2 text-label">
|
||||
콘텐츠명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={contentsQuery.contsTtl}
|
||||
onChange={(e) => setContentsQuery({ contsTtl: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 목록</h3>
|
||||
<Button onClick={() => router.push("/contents/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-auto !p-0">
|
||||
<DataTable
|
||||
table={table}
|
||||
onRowClick={handleRowClick}
|
||||
variant="secondary"
|
||||
viewPagination
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentsPage };
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
ContentsApi,
|
||||
contentsFormSchema,
|
||||
type ContentsFormData,
|
||||
} from "@/entities/content";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ContentsForm } from "./contents-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ContentsFormData>({
|
||||
resolver: zodResolver(contentsFormSchema),
|
||||
defaultValues: {
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">콘텐츠 등록</h3>
|
||||
</div>
|
||||
<ContentsForm form={form} handleSave={createContents} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,182 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
CONTENTS_APPROVE_STATUS,
|
||||
ContentsApi,
|
||||
contentsFormSchema,
|
||||
type ContentsFormData,
|
||||
} from "@/entities/content";
|
||||
import { ContentsForm } from "./contents-form";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
Button,
|
||||
} from "@common/components/ui";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const contsSn = Number(sequential);
|
||||
const [contsVer, setContsVer] = useState(0);
|
||||
|
||||
const { data: contentsVersionList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
queryFn: () =>
|
||||
ContentsApi.getContentsVersionList({
|
||||
contsSn: contsSn,
|
||||
page: 0,
|
||||
size: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
const { data: contentsDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsVer],
|
||||
queryFn: () => ContentsApi.getLatestContentsInfo(contsSn, contsVer),
|
||||
enabled: !!contsVer,
|
||||
});
|
||||
|
||||
const { mutateAsync: createContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateContents } = useMutation({
|
||||
mutationFn: (data: ContentsFormData) =>
|
||||
ContentsApi.updateContents(contsSn, contsVer, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteContents } = useMutation({
|
||||
mutationFn: () => ContentsApi.deleteContents(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
setContsVer(0);
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contsSn],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: requestApprove } = useMutation({
|
||||
mutationFn: () => ContentsApi.requestApprove(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
alert("승인 요청이 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: approveContents } = useMutation({
|
||||
mutationFn: () => ContentsApi.approveContents(contsSn, contsVer),
|
||||
onSuccess: () => {
|
||||
alert("승인되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ContentsFormData>({
|
||||
resolver: zodResolver(contentsFormSchema),
|
||||
values: contentsDetail,
|
||||
});
|
||||
|
||||
const approveStatus = contentsDetail?.aprvCd;
|
||||
const isContsTtlDisabled =
|
||||
(contentsVersionList?.content?.[0]?.contsVer ?? 0) > 1;
|
||||
|
||||
const onRequestApprove = async () => {
|
||||
const message =
|
||||
"승인 요청하면 더이상 내용을 수정할 수 없습니다.\n요청하시겠습니까?";
|
||||
if (await confirm(message)) requestApprove();
|
||||
};
|
||||
|
||||
const onApprove = async () => {
|
||||
const message = "승인하시겠습니까?";
|
||||
if (await confirm(message)) approveContents();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (contsVer) return;
|
||||
if (!contentsVersionList?.content?.length) return;
|
||||
|
||||
const latestContent = contentsVersionList?.content[0];
|
||||
if (!latestContent) return;
|
||||
|
||||
setContsVer(latestContent?.contsVer ?? 0);
|
||||
}, [contentsVersionList?.content]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">템플릿 수정</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={contsVer.toString()}
|
||||
onValueChange={(value) => {
|
||||
setContsVer(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[28rem]">
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{contentsVersionList?.content?.map((item) => (
|
||||
<SelectItem
|
||||
key={item.contsVer}
|
||||
value={item.contsVer.toString()}
|
||||
>
|
||||
{item.contsCn}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{approveStatus === CONTENTS_APPROVE_STATUS.DRAFT && (
|
||||
<Button onClick={onRequestApprove}>승인요청</Button>
|
||||
)}
|
||||
{approveStatus === CONTENTS_APPROVE_STATUS.REQUESTED && (
|
||||
<>
|
||||
<Button onClick={onApprove}>승인</Button>
|
||||
<Button color="gray">반려</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ContentsForm
|
||||
form={form}
|
||||
contsSn={contsSn}
|
||||
isEditable={approveStatus !== CONTENTS_APPROVE_STATUS.REQUESTED}
|
||||
isContsTtlDisabled={isContsTtlDisabled}
|
||||
handleSave={
|
||||
approveStatus === CONTENTS_APPROVE_STATUS.APPROVED
|
||||
? createContents
|
||||
: updateContents
|
||||
}
|
||||
handleDelete={deleteContents}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1 +0,0 @@
|
||||
export { DashboardPage } from "./ui/dashboard-page";
|
||||
@@ -1,5 +0,0 @@
|
||||
function DashboardPage() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
@@ -1 +0,0 @@
|
||||
export { FilePage } from "./ui/file-page";
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@common/components";
|
||||
import type { Attachment } from "@/entities/attachment";
|
||||
|
||||
export const createColumns = (
|
||||
handleDeleteAttachment: (atflId: string) => void,
|
||||
): ColumnDef<Attachment>[] => [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: (info) => info.row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "atflId",
|
||||
header: "파일ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "atflSn",
|
||||
header: "파일순번",
|
||||
},
|
||||
{
|
||||
accessorKey: "orgnlFileNm",
|
||||
header: "파일명",
|
||||
},
|
||||
{
|
||||
accessorKey: "atflFlieSz",
|
||||
header: "파일크기",
|
||||
cell: ({ row }) => {
|
||||
const size = row.getValue("atflFlieSz");
|
||||
return size ? formatFileSize(Number(size)) : "0 Bytes";
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "dwnldNocs",
|
||||
header: "다운로드수",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "등록일시",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("regDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "delYn",
|
||||
header: "삭제여부",
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
header: "삭제",
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
onClick={() => handleDeleteAttachment(row.original.atflId)}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
console.log("bytes: ", bytes);
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { AttachmentApi, type AttachmentParams } from "@/entities/attachment";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { createColumns } from "../model/table-columns";
|
||||
|
||||
function FilePage() {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [attachmentQuery, setAttachmentQuery] = useState<AttachmentParams>({
|
||||
keyword: "",
|
||||
});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: attachmentsList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS],
|
||||
queryFn: () =>
|
||||
AttachmentApi.getAttachmentsList({
|
||||
...attachmentQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const { mutate: deleteAttachment } = useMutation({
|
||||
mutationFn: (atflId: string) => AttachmentApi.deleteAttachment(atflId),
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteAttachment = async (atflId: string) => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
deleteAttachment(atflId);
|
||||
}
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data: attachmentsList?.content ?? [],
|
||||
columns: createColumns(handleDeleteAttachment),
|
||||
rowCount: attachmentsList?.totalElements ?? 0,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
});
|
||||
|
||||
const handleAttachmentQueryChange =
|
||||
(key: keyof AttachmentParams) => (value: string) => {
|
||||
setAttachmentQuery({ ...attachmentQuery, [key]: value });
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card card-border">
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 p-2">
|
||||
<div className="flex space-x-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="keyword" className="text-label">
|
||||
파일명
|
||||
</Label>
|
||||
<Input
|
||||
id="keyword"
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={attachmentQuery.keyword || ""}
|
||||
onChange={(e) =>
|
||||
handleAttachmentQueryChange("keyword")(e.target.value)
|
||||
}
|
||||
placeholder="파일명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">파일 목록</h3>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] overflow-auto !p-0">
|
||||
<DataTable table={table} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { FilePage };
|
||||
@@ -1,47 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Image,
|
||||
ImageFormData,
|
||||
ImageList,
|
||||
ImageParams,
|
||||
ImageService,
|
||||
} from "../model/image-interface";
|
||||
|
||||
export class ImageApiService implements ImageService {
|
||||
public async getImageList(params: ImageParams): Promise<ImageList> {
|
||||
const response = await client.get<ImageList>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getImageInfo(imgSn: string): Promise<Image> {
|
||||
const response = await client.get<Image>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createImage(data: ImageFormData): Promise<void> {
|
||||
const response = await client.post<void>(ENDPOINT.CMS_SERVICE.IMAGES, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateImage(imgSn: string, data: ImageFormData): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteImage(imgSn: string): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ImageApi = new ImageApiService();
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ImagePage } from "./ui/image-page";
|
||||
@@ -1,52 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const IMAGE_TYPE = {
|
||||
IMGSUB: "IMGSUB",
|
||||
IMGMAIN: "IMGMAIN",
|
||||
IMGLOGIN: "IMGLOGIN",
|
||||
} as const;
|
||||
|
||||
export type ImageType = (typeof IMAGE_TYPE)[keyof typeof IMAGE_TYPE];
|
||||
|
||||
export interface ImageParams extends PageableParams {
|
||||
keyword: string;
|
||||
imgTypeCd?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
imgSn: number;
|
||||
imgTypeCd: ImageType;
|
||||
imgTypeNm: string;
|
||||
imgTtl: string;
|
||||
lnkgUrlAddr: string;
|
||||
atflYn: string;
|
||||
sortSeq: number;
|
||||
atflCd: string;
|
||||
useYn: string;
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const imageFormSchema = z.object({
|
||||
imgTtl: z.string(),
|
||||
sortSeq: z.number(),
|
||||
imgTypeCd: z.string(),
|
||||
atflYn: z.string(),
|
||||
atflCd: z.string().nullish(),
|
||||
lnkgUrlAddr: z.string().nullish(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type ImageFormData = z.infer<typeof imageFormSchema>;
|
||||
export type ImageList = PageableList<Image>;
|
||||
|
||||
export interface ImageService {
|
||||
getImageList: (params: ImageParams) => Promise<ImageList>;
|
||||
getImageInfo: (imgSn: string) => Promise<Image>;
|
||||
createImage: (data: ImageFormData) => Promise<void>;
|
||||
updateImage: (imgSn: string, data: ImageFormData) => Promise<void>;
|
||||
deleteImage: (imgSn: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import { type Image } from "./image-interface";
|
||||
import { IMAGE_TYPE } from "./image-interface";
|
||||
|
||||
export const columns: ColumnDef<Image>[] = [
|
||||
{
|
||||
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: "imgSn",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "imgTypeCd",
|
||||
header: "이미지구분",
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue("imgTypeCd");
|
||||
switch (type) {
|
||||
case IMAGE_TYPE.IMGSUB:
|
||||
return "서브이미지";
|
||||
case IMAGE_TYPE.IMGMAIN:
|
||||
return "메인이미지";
|
||||
case IMAGE_TYPE.IMGLOGIN:
|
||||
return "로그인이미지";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "imgTtl",
|
||||
header: "이미지명",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,44 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Post } from "@/entities/posting";
|
||||
|
||||
export const columns: 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 }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "siteNm",
|
||||
header: "사이트",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstgPrdSeNm",
|
||||
header: "게시기간",
|
||||
cell: ({ row }) => {
|
||||
const pstgPrdSeNm = row.original.pstgPrdSeNm;
|
||||
const pstgBgngDt = row.original.pstgBgngDt;
|
||||
const pstgEndDt = row.original.pstgEndDt;
|
||||
return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,40 +0,0 @@
|
||||
"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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.createImage(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
defaultValues: {
|
||||
atflYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 등록</h3>
|
||||
</div>
|
||||
<ImageForm form={form} handleSave={createImage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,307 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse } from "tus-js-client";
|
||||
import {
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { AttachmentApi } from "@/entities/attachment";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useTus } from "@/shared/lib/use-tus";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { useAttachmentStore } from "@/shared/lib/use-attachment-store";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
|
||||
type ImageFormProps = {
|
||||
form: UseFormReturn<ImageFormData>;
|
||||
imgSn?: number;
|
||||
handleSave: (data: ImageFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
|
||||
const atflYn = form.watch("atflYn");
|
||||
const atflCd = form.watch("atflCd");
|
||||
|
||||
const {
|
||||
file: uploadFile,
|
||||
response: uploadResponse,
|
||||
progress,
|
||||
setFile,
|
||||
} = useTus();
|
||||
const { setAttachmentFileCode } = useAttachmentStore((state) => state);
|
||||
const [imageUrl, setImageUrl] = useState("");
|
||||
|
||||
const { data: attachmentData } = useQuery({
|
||||
queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd],
|
||||
queryFn: () => AttachmentApi.getAttachmentsInfo(atflCd!),
|
||||
select: (data) => data?.[data.length - 1],
|
||||
enabled: !!atflCd,
|
||||
});
|
||||
|
||||
const handleUploadResponse = async (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
const { atflCd, atflId } = JSON.parse(fileInfo) ?? {};
|
||||
if (atflCd) form.setValue("atflCd", atflCd);
|
||||
if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId));
|
||||
if (attachmentData)
|
||||
await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!uploadResponse) return;
|
||||
handleUploadResponse(uploadResponse);
|
||||
}, [uploadResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setImageUrl(
|
||||
await AttachmentApi.getImageUrlByAtflCd(attachmentData?.atflCd ?? ""),
|
||||
);
|
||||
})();
|
||||
}, [attachmentData]);
|
||||
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!imgSn} {...props}>
|
||||
<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="imgTtl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="이미지명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="정렬순서 입력"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이미지유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="imgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="IMGTYPE"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>첨부파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="atflYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="atflY" />
|
||||
<Label htmlFor="atflY">이미지파일</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="atflN" />
|
||||
<Label htmlFor="atflN">외부링크</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{atflYn === "Y" && (
|
||||
<TableRow>
|
||||
<TableHead>이미지파일</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="atflCd"
|
||||
render={() => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
id="atflCd"
|
||||
className="!h-auto !max-w-none flex-col !items-start"
|
||||
accept=".jpeg,.jpg,.png"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
setAttachmentFileCode("");
|
||||
setFile(files[0] ?? null);
|
||||
}}
|
||||
customPreview={
|
||||
<>
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] overflow-hidden rounded-sm border border-border bg-accent">
|
||||
<div className="relative h-full w-full">
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
className="object-contain"
|
||||
fill
|
||||
alt="업로드된 이미지"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="absolute bottom-3 left-0 z-10 p-2 text-base leading-none">
|
||||
{uploadFile?.name}
|
||||
</p>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0 z-10"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<div className="mb-2 text-lg font-medium text-label">
|
||||
<ul className="ml-10 list-disc">
|
||||
<li>권장 사이즈: 1920x500</li>
|
||||
<li>파일 최대 크기: 2MB</li>
|
||||
<li>확장자: jpg, jpeg, png 가능</li>
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{atflYn === "N" && (
|
||||
<TableRow>
|
||||
<TableHead>외부링크</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lnkgUrlAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="외부링크를 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">미사용</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImageForm };
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type PaginationState,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { POSTING_TYPE, PostingDialog } from "@/entities/posting";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/image-table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
type PostingTarget = {
|
||||
pstgTrgtNm: string;
|
||||
pstgTrgtNo: number;
|
||||
};
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [postingTargets, setPostingTargets] = useState<PostingTarget[]>([]);
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({ keyword: "" });
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 5,
|
||||
});
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
queryFn: () =>
|
||||
ImageApi.getImageList({
|
||||
...imageQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: imageList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
rowCount: imageList?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const postingTargets = table
|
||||
.getFilteredSelectedRowModel()
|
||||
.rows.map((row) => ({
|
||||
pstgTrgtNm: row.original.imgTtl,
|
||||
pstgTrgtNo: row.original.imgSn,
|
||||
}));
|
||||
|
||||
if (!postingTargets.length) {
|
||||
setPostingTargets([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setPostingTargets(postingTargets);
|
||||
}, [rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [pagination]);
|
||||
|
||||
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 htmlFor="" className="mr-2 text-label">
|
||||
이미지명
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]"
|
||||
value={imageQuery.keyword}
|
||||
onChange={(e) =>
|
||||
setImageQuery({ ...imageQuery, keyword: e.target.value })
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
<PostingDialog
|
||||
postingType={POSTING_TYPE.IMAGE}
|
||||
postingTargets={postingTargets}
|
||||
/>
|
||||
<Button onClick={() => router.push("/image/register")}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-full overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} viewPagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImagePage };
|
||||
@@ -1,128 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button, DataTable } from "@common/components/ui";
|
||||
import { PostingApi, type PostPayload } from "@/entities/posting";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { columns } from "../model/posting-table-columns";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: imageDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, sequential],
|
||||
queryFn: () => ImageApi.getImageInfo(sequential),
|
||||
});
|
||||
|
||||
const { data: postHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
queryFn: () =>
|
||||
PostingApi.getPostingHistory({ pstgTrgtNo: Number(sequential) }),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteImage } = useMutation({
|
||||
mutationFn: () => ImageApi.deleteImage(sequential),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: savePosting } = useMutation({
|
||||
mutationFn: (data: PostPayload[]) => PostingApi.savePosting(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.POSTINGS, sequential],
|
||||
});
|
||||
alert("삭제되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: postHistory?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleDeletePostingHistory = async () => {
|
||||
const data = table.getFilteredSelectedRowModel().rows.map((row) => ({
|
||||
...row.original,
|
||||
mode: "D",
|
||||
}));
|
||||
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await savePosting(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
|
||||
</div>
|
||||
<ImageForm
|
||||
form={form}
|
||||
imgSn={Number(sequential)}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 게시 이력</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
disabled={!table.getFilteredSelectedRowModel().rows.length}
|
||||
onClick={handleDeletePostingHistory}
|
||||
>
|
||||
이력삭제
|
||||
</Button>
|
||||
</div>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,65 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
Menu,
|
||||
MenuFormData,
|
||||
MenuService,
|
||||
TreeMenu,
|
||||
TreeNode,
|
||||
} from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
public async getMenuInfo(menuCd: number): Promise<Menu> {
|
||||
const response = await client.get<Menu>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createMenu(data: MenuFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenu(
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteMenu(menuCd: number): Promise<void> {
|
||||
await client.delete(`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}`);
|
||||
}
|
||||
|
||||
public async getTreeMenuList(siteId: string): Promise<TreeMenu[]> {
|
||||
const response = await client.get<TreeMenu[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateMenuName(menuCd: number, name: string): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -1 +0,0 @@
|
||||
export { MenuPage } from "./ui/menu-page";
|
||||
@@ -1,171 +0,0 @@
|
||||
import type { DropOptions, NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import type {
|
||||
TreeUserOrganization,
|
||||
TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import type { TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
|
||||
export const generateMenuTreeData = (
|
||||
data: TreeMenu[],
|
||||
): NodeModel<TreeMenu>[] => {
|
||||
return data.reduce<NodeModel<TreeMenu>[]>((acc, menu) => {
|
||||
const node: NodeModel<TreeMenu> = {
|
||||
id: menu.menuCd ?? 0,
|
||||
parent: menu.parentId ?? 0,
|
||||
text: menu.menuKornNm,
|
||||
droppable: true,
|
||||
data: menu,
|
||||
};
|
||||
|
||||
acc.push(node);
|
||||
|
||||
if (menu.children?.length) {
|
||||
acc.push(...generateMenuTreeData(menu.children));
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const generateNewMenuTreeData = (
|
||||
treeData: NodeModel<TreeMenu>[],
|
||||
options: DropOptions<TreeMenu>,
|
||||
): TreeNode | null => {
|
||||
const { dragSourceId, dragSource, dropTargetId, dropTarget } = options;
|
||||
|
||||
if (!dragSource?.data) return null;
|
||||
|
||||
const siblings = treeData.filter((node) => node.parent === dropTargetId);
|
||||
|
||||
const dragSourceIndex = siblings.findIndex(
|
||||
(node) => node.id === dragSourceId,
|
||||
);
|
||||
|
||||
const prevSibling = siblings[dragSourceIndex - 1];
|
||||
|
||||
const newSortSeq =
|
||||
dragSourceIndex === 0 ? 1 : (prevSibling?.data?.sortSeq ?? 0) + 1;
|
||||
|
||||
const childNodes = treeData.filter(
|
||||
(node) => node.parent === dragSourceId && node.data,
|
||||
);
|
||||
|
||||
const baseNode = (node: NodeModel<TreeMenu>): TreeNode => {
|
||||
if (!node.data) return {};
|
||||
return {
|
||||
menuCd: node.data?.menuCd,
|
||||
name: node.data?.menuKornNm,
|
||||
icon: node.data?.iconNm,
|
||||
};
|
||||
};
|
||||
|
||||
const treeNode: TreeNode = {
|
||||
...baseNode(dragSource),
|
||||
parentId: dropTargetId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: newSortSeq,
|
||||
levelNo: (dropTarget?.data?.levelNo ?? 0) + 1,
|
||||
};
|
||||
|
||||
const result: TreeNode = {
|
||||
...treeNode,
|
||||
children: childNodes.map((child) => ({
|
||||
...baseNode(child),
|
||||
parentId: child.data?.parentId === 0 ? null : (dropTargetId as number),
|
||||
sortSeq: child.data?.sortSeq,
|
||||
levelNo: (treeNode?.levelNo ?? 0) + 1,
|
||||
})),
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const handleSelectNode =
|
||||
(callback: (node: NodeModel<TreeMenu>) => void) =>
|
||||
(node: NodeModel<TreeMenu>) => {
|
||||
callback(node);
|
||||
};
|
||||
|
||||
export const handleDropNode =
|
||||
(callback: (newTree: TreeNode) => void) =>
|
||||
(newTree: NodeModel<TreeMenu>[], options: DropOptions<TreeMenu>) => {
|
||||
const newTreeData = generateNewMenuTreeData(newTree, options);
|
||||
if (!newTreeData) return;
|
||||
callback(newTreeData);
|
||||
};
|
||||
|
||||
export const handleNodeTextChange =
|
||||
(treeData: NodeModel<TreeMenu>[]) =>
|
||||
(callback: (newTree: NodeModel<TreeMenu>[]) => void) =>
|
||||
(id: NodeModel["id"], value: string) => {
|
||||
const newTree = treeData.map((node) => {
|
||||
if (node.id === id) {
|
||||
return {
|
||||
...node,
|
||||
text: value,
|
||||
};
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
callback(newTree);
|
||||
};
|
||||
|
||||
export const generateUserOrganizationTreeData = (
|
||||
organizations: TreeUserOrganization[],
|
||||
): NodeModel<TreeUserOrganization | TreeUser>[] => {
|
||||
return organizations.reduce<NodeModel<TreeUserOrganization | TreeUser>[]>(
|
||||
(acc, org) => {
|
||||
const orgNode: NodeModel<TreeUserOrganization> = {
|
||||
id: org.ognzSn,
|
||||
parent: org.upOgnzSn ?? 0,
|
||||
text: org.ognzNm,
|
||||
data: org,
|
||||
droppable: false,
|
||||
};
|
||||
|
||||
acc.push(orgNode);
|
||||
|
||||
if (org.children?.length) {
|
||||
acc.push(...generateUserOrganizationTreeData(org.children));
|
||||
}
|
||||
|
||||
if (org.users?.length) {
|
||||
acc.push(
|
||||
...org.users.map((user) => ({
|
||||
id: user.userUnqId,
|
||||
parent: org.ognzSn,
|
||||
text: user.userNm,
|
||||
data: user,
|
||||
droppable: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
export const filterUserOrganizationTreeDataByKeyword = (
|
||||
treeData: NodeModel<TreeUserOrganization | TreeUser>[],
|
||||
keyword: string,
|
||||
): NodeModel<TreeUserOrganization | TreeUser>[] => {
|
||||
return Array.from(
|
||||
treeData.reduce((matchedNodes, node) => {
|
||||
if (!node.text.includes(keyword)) return matchedNodes;
|
||||
|
||||
matchedNodes.add(node);
|
||||
|
||||
let parentId = node.parent;
|
||||
while (parentId) {
|
||||
const parentNode = treeData.find((n) => n.id === parentId);
|
||||
if (!parentNode) break;
|
||||
matchedNodes.add(parentNode);
|
||||
parentId = parentNode.parent;
|
||||
}
|
||||
|
||||
return matchedNodes;
|
||||
}, new Set<NodeModel<TreeUserOrganization | TreeUser>>()),
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
|
||||
export const columns: ColumnDef<BoardPopup>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsNm",
|
||||
header: "게시판명",
|
||||
},
|
||||
{
|
||||
accessorKey: "bbsTypeNm",
|
||||
header: "게시판유형",
|
||||
},
|
||||
{
|
||||
accessorKey: "regDt",
|
||||
header: "최초생성일",
|
||||
},
|
||||
{
|
||||
accessorKey: "pstCnt",
|
||||
header: "게시물건수",
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
className="mx-4 flex justify-center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,54 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Contents } from "@/entities/content";
|
||||
|
||||
export const columns: ColumnDef<Contents>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
id: "contsTtl",
|
||||
header: "콘텐츠명",
|
||||
cell: ({ row }) => row.original.contsTtl,
|
||||
},
|
||||
{
|
||||
accessorKey: "rgTrNm",
|
||||
header: "승인자",
|
||||
},
|
||||
{
|
||||
accessorKey: "aprvDt",
|
||||
header: "승인일자",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.aprvDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.mdfcnDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: "선택",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
className="mx-4 flex justify-center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,86 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const MENU_TYPE = {
|
||||
EMPTY: "EMPTY",
|
||||
CONTENTS: "CONTENTS",
|
||||
BOARD: "BOARD",
|
||||
INSIDE: "INSIDE",
|
||||
OUTSIDE: "OUTSIDE",
|
||||
BOTTOM: "BOTTOM",
|
||||
} as const;
|
||||
|
||||
export type MenuType = (typeof MENU_TYPE)[keyof typeof MENU_TYPE];
|
||||
|
||||
export interface TreeNode {
|
||||
menuCd?: number;
|
||||
name?: string;
|
||||
sortSeq?: number;
|
||||
parentId?: number | null;
|
||||
levelNo?: number;
|
||||
icon?: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
export interface TreeMenu extends Omit<TreeNode, "name" | "icon" | "children"> {
|
||||
menuKornNm: string;
|
||||
menuEngNm?: string;
|
||||
iconNm?: string;
|
||||
children?: TreeMenu[];
|
||||
urlPath?: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn?: number;
|
||||
qrcd?: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
requiredUrlPath?: boolean;
|
||||
}
|
||||
|
||||
export const menuFormSchema = z.object({
|
||||
siteId: z.string(),
|
||||
menuKornNm: z.string(),
|
||||
menuSubKornNm: z.string().nullish(),
|
||||
menuEngNm: z.string().nullish(),
|
||||
menuSubEngNm: z.string().nullish(),
|
||||
levelNo: z.number(),
|
||||
urlPath: z.string().nullish(),
|
||||
iconNm: z.string().nullish(),
|
||||
menuLnkgTypeCd: z.enum([
|
||||
MENU_TYPE.EMPTY,
|
||||
MENU_TYPE.CONTENTS,
|
||||
MENU_TYPE.BOARD,
|
||||
MENU_TYPE.INSIDE,
|
||||
MENU_TYPE.OUTSIDE,
|
||||
MENU_TYPE.BOTTOM,
|
||||
]),
|
||||
menuLnkgSn: z.number().nullish(),
|
||||
otptYn: z.string().default("Y"),
|
||||
prntMenuCd: z.number().nullish(),
|
||||
sortSeq: z.number(),
|
||||
evlUseYn: z.string().default("Y"),
|
||||
qrcd: z.string().nullish(),
|
||||
useYn: z.string().default("Y"),
|
||||
newWndwYn: z.string().nullish(),
|
||||
menuLnkgSnNm: z.string().nullish(),
|
||||
menuPicUserUnqId: z.string().nullish(),
|
||||
menuPicUserUnqIdNm: z.string().nullish(),
|
||||
menuPicDsgnYmd: z.string().nullish(),
|
||||
});
|
||||
|
||||
export type MenuFormData = z.infer<typeof menuFormSchema>;
|
||||
export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
menuCd: number;
|
||||
iconName?: string;
|
||||
};
|
||||
|
||||
export interface MenuService {
|
||||
getMenuInfo: (menuCd: number) => Promise<Menu>;
|
||||
createMenu: (data: MenuFormData) => Promise<void>;
|
||||
updateMenu: (
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
) => Promise<void>;
|
||||
deleteMenu: (menuCd: number) => Promise<void>;
|
||||
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
|
||||
updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
updateMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
|
||||
interface BaseMenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuCd?: number;
|
||||
}
|
||||
|
||||
function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
return (
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{menuCd && (
|
||||
<TableRow>
|
||||
<TableHead>메뉴ID</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{menuCd}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>메뉴명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuKornNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="메뉴명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 영문명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuEngNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="메뉴 영문명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 유형</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuLnkgTypeCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{Object.entries(MENU_TYPE).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<RadioGroupItem value={value} id={key} />
|
||||
<Label htmlFor={key}>
|
||||
{key === "EMPTY" && "빈메뉴"}
|
||||
{key === "CONTENTS" && "콘텐츠"}
|
||||
{key === "BOARD" && "게시판"}
|
||||
{key === "INSIDE" && "내부링크"}
|
||||
{key === "OUTSIDE" && "외부링크"}
|
||||
{key === "BOTTOM" && "하단메뉴"}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { BaseMenuForm };
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { BoardApi, type BoardPopup } from "@/entities/board";
|
||||
import { columns } from "../model/board-table-columns";
|
||||
|
||||
type BoardSearchDialogProps = {
|
||||
handleBoardSelect: (board: BoardPopup) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function BoardSearchDialog({
|
||||
handleBoardSelect,
|
||||
...props
|
||||
}: BoardSearchDialogProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [bbsNm, setBbsNm] = useState("");
|
||||
|
||||
const { data: boardList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.BOARD_SERVICE.BBS],
|
||||
queryFn: () => BoardApi.getBoardPopupList({ bbsNm }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: boardList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const boardData = table.getSelectedRowModel().rows[0]
|
||||
?.original as BoardPopup;
|
||||
if (!boardData) return;
|
||||
handleBoardSelect(boardData);
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>게시판 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={bbsNm}
|
||||
onChange={(e) => setBbsNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { BoardSearchDialog };
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ContentsApi, type Contents } from "@/entities/content";
|
||||
import { columns } from "../model/contents-table-colums";
|
||||
|
||||
type ContentSearchDialogProps = {
|
||||
handleContentSelect: (content: Contents) => void;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ContentSearchDialog({
|
||||
handleContentSelect,
|
||||
...props
|
||||
}: ContentSearchDialogProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [contsTtl, setContsTtl] = useState("");
|
||||
|
||||
const { data: contentList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS],
|
||||
queryFn: () => ContentsApi.getContentsList({ contsTtl }),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: contentList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const contentData = table.getSelectedRowModel().rows[0]
|
||||
?.original as Contents;
|
||||
if (!contentData) return;
|
||||
handleContentSelect(contentData);
|
||||
}, [table.getSelectedRowModel().rows]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={contsTtl}
|
||||
onChange={(e) => setContsTtl(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentSearchDialog };
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import { Button, Input } from "@common/components/ui";
|
||||
|
||||
type EditableNodeProps = {
|
||||
depth?: number;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
} & ComponentProps<typeof Input>;
|
||||
|
||||
function EditableNode({
|
||||
depth,
|
||||
onSave,
|
||||
onCancel,
|
||||
...props
|
||||
}: EditableNodeProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-sm bg-accent p-2"
|
||||
style={{ marginInlineStart: !depth ? 0 : depth * 5 + 5 }}
|
||||
>
|
||||
<Input {...props} className="h-[2.5rem] !text-[1.3rem] text-foreground" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onSave}
|
||||
>
|
||||
<i className="diveicon di-check text-2xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 !bg-transparent"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<i className="diveicon di-x" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { EditableNode };
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { Button } from "@common/components";
|
||||
import { Form } from "@/shared/ui/form";
|
||||
import { ManagementAuthorityDialog } from "@/entities/menu-authority";
|
||||
import type { MenuFormData } from "../model/menu-interface";
|
||||
import { BaseMenuForm } from "./base-menu-form";
|
||||
import { TypeMenuForm } from "./type-menu-form";
|
||||
|
||||
interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuCd?: number;
|
||||
handleCreate: (data: MenuFormData) => void;
|
||||
handleUpdate: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => void;
|
||||
handleDelete?: (menuCd: number) => void;
|
||||
}
|
||||
|
||||
function MenuForm({
|
||||
form,
|
||||
menuCd,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
}: MenuFormProps) {
|
||||
const isModifyMode = !!menuCd;
|
||||
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
if (isModifyMode) {
|
||||
await onUpdate(data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreate = async (data: MenuFormData) => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
handleCreate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.(menuCd!);
|
||||
}
|
||||
};
|
||||
|
||||
const menuType = form.watch("menuLnkgTypeCd");
|
||||
const isSubmitDisabled = !form.watch("siteId");
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
{isModifyMode ? "메뉴 수정" : "메뉴 등록"}
|
||||
</h3>
|
||||
<ManagementAuthorityDialog menuCd={menuCd} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<BaseMenuForm form={form} menuCd={menuCd} />
|
||||
<TypeMenuForm form={form} menuType={menuType} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" type="submit" disabled={isSubmitDisabled}>
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuForm };
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { NodeModel, TreeMethods } from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuNodeProps = {
|
||||
node: NodeModel<TreeMenu>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
isAdditionalParent: boolean;
|
||||
treeMethods: TreeMethods | null;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<TreeMenu>) => void;
|
||||
};
|
||||
|
||||
function MenuNode({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
isSelected,
|
||||
isAdditionalParent,
|
||||
treeMethods,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: MenuNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 10 + 5;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: updateMenuName } = useMutation({
|
||||
mutationFn: (nodeName: string) => {
|
||||
const encodedName = encodeURIComponent(nodeName);
|
||||
return MenuApi.updateMenuName(id as number, encodedName);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(text);
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
const handleSave = async () => {
|
||||
await updateMenuName(nodeName);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditMode(false);
|
||||
setNodeName(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdditionalParent) treeMethods?.open(id);
|
||||
}, [isAdditionalParent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className={cn(
|
||||
"group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
|
||||
isSelected && "!bg-[rgba(32,148,250,0.2)]",
|
||||
)}
|
||||
>
|
||||
{editMode ? (
|
||||
<EditableNode
|
||||
value={nodeName}
|
||||
onChange={(e) => setNodeName(e.target.value)}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center" onClick={handleSelect}>
|
||||
{data?.children?.length && (
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="invisible !bg-transparent group-hover:visible"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
<i className="diveicon di-pencil" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuNode };
|
||||
@@ -1,256 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { NodeModel } from "@minoru/react-dnd-treeview";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components/ui";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import {
|
||||
MENU_TYPE,
|
||||
menuFormSchema,
|
||||
type MenuFormData,
|
||||
} from "../model/menu-interface";
|
||||
import { MenuTree } from "./menu-tree";
|
||||
import { MenuForm } from "./menu-form";
|
||||
import { generateMenuTreeData } from "../lib/utils";
|
||||
|
||||
function MenuPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const [siteId, setSiteId] = useState("");
|
||||
const [selectedMenu, setSelectedMenu] = useState<TreeMenu | null>(null);
|
||||
const [additionalNode, setAdditionalNode] =
|
||||
useState<NodeModel<TreeMenu> | null>(null);
|
||||
|
||||
const { data: menuInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, selectedMenu?.menuCd],
|
||||
queryFn: () => MenuApi.getMenuInfo(selectedMenu?.menuCd ?? 0),
|
||||
enabled: !!selectedMenu?.menuCd,
|
||||
});
|
||||
|
||||
const { data: menuTreeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
queryFn: () => MenuApi.getTreeMenuList(siteId),
|
||||
select: (data) => {
|
||||
const treeData = generateMenuTreeData(data);
|
||||
const defaultSortSeq =
|
||||
treeData.filter((node) => node.data?.levelNo === 1).length + 1;
|
||||
form.setValue("sortSeq", defaultSortSeq);
|
||||
|
||||
return treeData;
|
||||
},
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
const { data: options } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
queryFn: () => SiteApi.getSiteList(),
|
||||
select: (data) =>
|
||||
data.content.map((site) => ({
|
||||
label: site.siteNm,
|
||||
value: site.siteId,
|
||||
})),
|
||||
});
|
||||
|
||||
const { mutateAsync: createMenu } = useMutation({
|
||||
mutationFn: (data: MenuFormData) => MenuApi.createMenu(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateMenu } = useMutation({
|
||||
mutationFn: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => MenuApi.updateMenu(menuCd, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteMenu } = useMutation({
|
||||
mutationFn: (menuCd: number) => MenuApi.deleteMenu(menuCd),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<MenuFormData>({
|
||||
resolver: zodResolver(menuFormSchema),
|
||||
defaultValues: {
|
||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||
},
|
||||
values: {
|
||||
...menuInfo,
|
||||
siteId,
|
||||
levelNo: menuInfo?.levelNo ?? 1,
|
||||
sortSeq: menuInfo?.sortSeq ?? 1,
|
||||
menuKornNm: menuInfo?.menuKornNm ?? "",
|
||||
menuLnkgTypeCd: menuInfo?.menuLnkgTypeCd ?? MENU_TYPE.EMPTY,
|
||||
iconNm: menuInfo?.iconName,
|
||||
otptYn: menuInfo?.otptYn ?? "Y",
|
||||
evlUseYn: menuInfo?.evlUseYn ?? "Y",
|
||||
useYn: menuInfo?.useYn ?? "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const additionalNodeForm = useForm<MenuFormData>({
|
||||
resolver: zodResolver(menuFormSchema),
|
||||
defaultValues: {
|
||||
menuLnkgTypeCd: MENU_TYPE.EMPTY,
|
||||
levelNo: 1,
|
||||
sortSeq: 1,
|
||||
otptYn: "Y",
|
||||
evlUseYn: "Y",
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdditionalNodeSave = async () => {
|
||||
const prntMenuCd = !additionalNode?.parent
|
||||
? null
|
||||
: (additionalNode?.parent as number);
|
||||
|
||||
await createMenu({
|
||||
...additionalNodeForm.getValues(),
|
||||
prntMenuCd,
|
||||
});
|
||||
resetAdditionalNode();
|
||||
};
|
||||
|
||||
const resetAdditionalNode = () => {
|
||||
additionalNodeForm.setValue("menuKornNm", "");
|
||||
setAdditionalNode(null);
|
||||
};
|
||||
|
||||
const handleSiteIdChange = (value: string) => {
|
||||
form.setValue("siteId", value);
|
||||
additionalNodeForm.setValue("siteId", value);
|
||||
|
||||
setSiteId(value);
|
||||
setSelectedMenu(null);
|
||||
};
|
||||
|
||||
const handleNodeSelect = (node: NodeModel<TreeMenu>) => {
|
||||
if (!node.data) return;
|
||||
if (node.data.menuCd === selectedMenu?.menuCd) {
|
||||
setSelectedMenu(null);
|
||||
additionalNodeForm.setValue("levelNo", 1);
|
||||
return;
|
||||
}
|
||||
setSelectedMenu(node.data);
|
||||
additionalNodeForm.setValue("levelNo", (node.data.levelNo ?? 0) + 1);
|
||||
};
|
||||
|
||||
const handleNodeDelete = async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
deleteMenu(selectedMenu!.menuCd!);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMenu = () => {
|
||||
if (additionalNode) return;
|
||||
setAdditionalNode({
|
||||
id: -1,
|
||||
parent: selectedMenu?.menuCd ?? 0,
|
||||
text: "",
|
||||
});
|
||||
};
|
||||
|
||||
const tree = additionalNode
|
||||
? [...(additionalNode ? [additionalNode] : []), ...(menuTreeData ?? [])]
|
||||
: (menuTreeData ?? []);
|
||||
|
||||
return (
|
||||
<div className="card !mb-0 flex !min-h-[calc(100vh-16.8rem)] gap-2">
|
||||
<div className="card card-border w-[24rem]">
|
||||
<Select value={siteId} onValueChange={handleSiteIdChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options?.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{siteId && (
|
||||
<>
|
||||
<div className="my-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="flex-1"
|
||||
onClick={handleAddMenu}
|
||||
>
|
||||
메뉴 추가
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
className="flex-1"
|
||||
onClick={handleNodeDelete}
|
||||
disabled={!selectedMenu}
|
||||
>
|
||||
메뉴 삭제
|
||||
</Button>
|
||||
</div>
|
||||
<div className="card card-border !mx-0">
|
||||
<MenuTree
|
||||
tree={tree}
|
||||
siteId={siteId}
|
||||
selectedMenu={selectedMenu}
|
||||
additionalNodeForm={additionalNodeForm}
|
||||
onSelectNode={handleNodeSelect}
|
||||
onAdditionalNodeSave={handleAdditionalNodeSave}
|
||||
onAdditionalNodeCancel={resetAdditionalNode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
menuCd={selectedMenu?.menuCd}
|
||||
handleCreate={createMenu}
|
||||
handleUpdate={updateMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuPage };
|
||||
@@ -1,151 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
type TreeMethods,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import type { MenuFormData, TreeMenu, TreeNode } from "../model/menu-interface";
|
||||
import { handleDropNode, handleSelectNode } from "../lib/utils";
|
||||
import { MenuNode } from "./menu-node";
|
||||
import { EditableNode } from "./editable-node";
|
||||
|
||||
type MenuTreeProps = {
|
||||
tree: NodeModel<TreeMenu>[];
|
||||
siteId: string;
|
||||
selectedMenu: TreeMenu | null;
|
||||
additionalNodeForm: UseFormReturn<MenuFormData>;
|
||||
onSelectNode: (node: NodeModel<TreeMenu>) => void;
|
||||
onAdditionalNodeSave: () => void;
|
||||
onAdditionalNodeCancel: () => void;
|
||||
};
|
||||
|
||||
function MenuTree({
|
||||
tree,
|
||||
siteId,
|
||||
selectedMenu,
|
||||
additionalNodeForm,
|
||||
onSelectNode,
|
||||
onAdditionalNodeSave,
|
||||
onAdditionalNodeCancel,
|
||||
}: MenuTreeProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const { mutate: updateTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeNode) =>
|
||||
MenuApi.updateTreeMenu(siteId, [newTree]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const additionalNode = tree.find((n) => n.id === -1);
|
||||
|
||||
const isAdditionalParent = (node: NodeModel<TreeMenu>): boolean => {
|
||||
if (!additionalNode) return false;
|
||||
return additionalNode?.parent === node.id;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-h-[calc(100vh-33.5rem)] overflow-auto">
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
ref={treeMethods}
|
||||
render={(
|
||||
node: NodeModel<TreeMenu>,
|
||||
{ depth, isOpen, onToggle },
|
||||
) => {
|
||||
if (node.id === -1) {
|
||||
return (
|
||||
<EditableNode
|
||||
value={additionalNodeForm.watch("menuKornNm") ?? ""}
|
||||
onChange={(e) =>
|
||||
additionalNodeForm.setValue("menuKornNm", e.target.value)
|
||||
}
|
||||
depth={depth}
|
||||
onSave={onAdditionalNodeSave}
|
||||
onCancel={onAdditionalNodeCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MenuNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
treeMethods={treeMethods.current}
|
||||
isOpen={isOpen}
|
||||
isSelected={selectedMenu?.menuCd === node.id}
|
||||
isAdditionalParent={isAdditionalParent(node)}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode(onSelectNode)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(updateTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
|
||||
placeholder: "relative",
|
||||
}}
|
||||
sort={false}
|
||||
dropTargetOffset={5}
|
||||
insertDroppableFirst={false}
|
||||
canDrop={(_, { dragSource, dropTargetId }) => {
|
||||
if (dragSource?.parent === dropTargetId) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
placeholderRender={(_, { depth }) => {
|
||||
return (
|
||||
<div
|
||||
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
|
||||
style={{ left: depth * 10 + 5 }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.openAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-expand" />
|
||||
전체펼침
|
||||
</Button>
|
||||
<Button
|
||||
color="lightGray"
|
||||
className="flex-1 items-center px-0"
|
||||
onClick={() => {
|
||||
treeMethods.current?.closeAll();
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrows-angle-contract" />
|
||||
전체접기
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { MenuTree };
|
||||
@@ -1,187 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import {
|
||||
Tree,
|
||||
MultiBackend,
|
||||
getBackendOptions,
|
||||
type NodeModel,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
} from "@common/components/ui";
|
||||
import { cn } from "@common/components/lib";
|
||||
import {
|
||||
UserOrganizationApi,
|
||||
type TreeUserOrganization,
|
||||
type TreeUser,
|
||||
} from "@/entities/user-organization";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
filterUserOrganizationTreeDataByKeyword,
|
||||
generateUserOrganizationTreeData,
|
||||
} from "../lib/utils";
|
||||
|
||||
type TreeModel = TreeUserOrganization | TreeUser;
|
||||
|
||||
interface OrganizationSearchDialogProps {
|
||||
handleUserSelect: (user: TreeUser) => void;
|
||||
}
|
||||
|
||||
function OrganizationSearchDialog({
|
||||
handleUserSelect,
|
||||
}: OrganizationSearchDialogProps) {
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [tree, setTree] = useState<NodeModel<TreeModel>[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<NodeModel<TreeModel> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { data: treeData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION],
|
||||
queryFn: () => UserOrganizationApi.getUserOrganizationTreeList(),
|
||||
select: (data) => generateUserOrganizationTreeData(data),
|
||||
});
|
||||
|
||||
const handleSelectNode = (node: NodeModel<TreeModel>) => {
|
||||
setSelectedNode(node);
|
||||
handleUserSelect(node.data as TreeUser);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTree(treeData ?? []);
|
||||
}, [treeData]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-3">
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[30rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>담당자 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
className="h-[2.5rem] !text-[1.3rem]"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setTree(filterUserOrganizationTreeDataByKeyword(tree, keyword))
|
||||
}
|
||||
>
|
||||
<i className="diveicon di-search text-base" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[36rem] overflow-auto">
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={tree}
|
||||
rootId={0}
|
||||
render={(
|
||||
node: NodeModel<TreeModel>,
|
||||
{ depth, isOpen, onToggle },
|
||||
) => (
|
||||
<TreeNode
|
||||
node={node}
|
||||
depth={depth}
|
||||
isOpen={isOpen}
|
||||
selectedNode={selectedNode}
|
||||
onToggle={onToggle}
|
||||
onSelect={handleSelectNode}
|
||||
/>
|
||||
)}
|
||||
classes={{
|
||||
container: "flex flex-col",
|
||||
}}
|
||||
sort={false}
|
||||
onDrop={() => {}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface TreeNodeProps {
|
||||
node: NodeModel<TreeModel>;
|
||||
depth: number;
|
||||
isOpen: boolean;
|
||||
selectedNode: NodeModel<TreeModel> | null;
|
||||
onToggle: (id: NodeModel["id"]) => void;
|
||||
onSelect: (node: NodeModel<TreeModel>) => void;
|
||||
}
|
||||
|
||||
const ORGANIZATION_PREFIX = "OGNZ";
|
||||
|
||||
function TreeNode({
|
||||
node,
|
||||
depth,
|
||||
isOpen,
|
||||
selectedNode,
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: TreeNodeProps) {
|
||||
const { id, text, data } = node;
|
||||
const indent = depth * 15 + 5;
|
||||
|
||||
const handleToggle = () => onToggle(id);
|
||||
const handleSelect = () => {
|
||||
if (id.toString().startsWith(ORGANIZATION_PREFIX)) return;
|
||||
onSelect(node);
|
||||
};
|
||||
|
||||
const isSelected = selectedNode?.id === id;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ paddingInlineStart: indent }}
|
||||
className="group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent"
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
{data && ("children" in data || "users" in data) && (
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-6 w-6 !bg-transparent"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon di-arrow-forward mr-2 text-base text-label",
|
||||
isOpen && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
<span className="text-[1.4rem] font-medium text-label">{text}</span>
|
||||
</div>
|
||||
{isSelected && <i className="diveicon di-check text-xl text-primary" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { OrganizationSearchDialog };
|
||||
@@ -1,368 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
DatePicker,
|
||||
IconFinder,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
import type { Contents } from "@/entities/content";
|
||||
import type { TreeUser } from "@/entities/user-organization";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { MenuFormData, MenuType } from "../model/menu-interface";
|
||||
import { OrganizationSearchDialog } from "./organization-search-dialog";
|
||||
import { ContentSearchDialog } from "./content-search-dialog";
|
||||
import { BoardSearchDialog } from "./board-search-dialog";
|
||||
|
||||
type TypeMenuFormProps = {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuType: MenuType;
|
||||
};
|
||||
|
||||
function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
const handleIconClick = (icon: string) => {
|
||||
form.setValue("iconNm", icon);
|
||||
};
|
||||
|
||||
const handleContentSelect = (content: Contents) => {
|
||||
form.setValue("menuLnkgSn", content.contsSn);
|
||||
form.setValue("menuLnkgSnNm", content.contsTtl);
|
||||
};
|
||||
|
||||
const handleUserSelect = (user: TreeUser) => {
|
||||
form.setValue("menuPicUserUnqId", user.userUnqId);
|
||||
form.setValue("menuPicUserUnqIdNm", user.userNm);
|
||||
};
|
||||
|
||||
const handleBoardSelect = (board: BoardPopup) => {
|
||||
form.setValue("menuLnkgSn", board.bbsId);
|
||||
form.setValue("menuLnkgSnNm", board.bbsNm);
|
||||
};
|
||||
|
||||
const handleDateSelect = (date: Date | undefined) => {
|
||||
if (!date) return;
|
||||
form.setValue("menuPicDsgnYmd", dayjs(date).format("YYYYMMDD"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Table variant="secondary" className="-mt-[0.1rem]">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
{menuType === "CONTENTS" && (
|
||||
<TableRow>
|
||||
<TableHead>콘텐츠</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-1/2 cursor-not-allowed overflow-hidden whitespace-nowrap break-keep rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuLnkgSnNm") &&
|
||||
"메뉴에 표시될 콘텐츠를 조회 후 선택하십시오."}
|
||||
{form.watch("menuLnkgSnNm")}
|
||||
</div>
|
||||
<ContentSearchDialog
|
||||
handleContentSelect={handleContentSelect}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{menuType === "BOARD" && (
|
||||
<TableRow>
|
||||
<TableHead>게시판</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-1/2 cursor-not-allowed overflow-hidden whitespace-nowrap break-keep rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuLnkgSnNm") &&
|
||||
"메뉴에 표시될 게시판을 조회 후 선택하십시오."}
|
||||
{form.watch("menuLnkgSnNm")}
|
||||
</div>
|
||||
<BoardSearchDialog handleBoardSelect={handleBoardSelect} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "INSIDE" || menuType === "OUTSIDE") && (
|
||||
<TableRow>
|
||||
<TableHead>연결 URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urlPath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder={
|
||||
menuType === "INSIDE"
|
||||
? "메뉴에 연결된 페이지의 경로를 입력하십시오."
|
||||
: "'http://' 또는 'https://'를 포함하여 입력하십시오."
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "CONTENTS" ||
|
||||
menuType === "BOARD" ||
|
||||
menuType === "INSIDE" ||
|
||||
menuType === "OUTSIDE") && (
|
||||
<TableRow>
|
||||
<TableHead>연결 형태</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newWndwYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="newWndwY" />
|
||||
<Label htmlFor="newWndwY">새창</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="newWndwN" />
|
||||
<Label htmlFor="newWndwN">현재창</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(menuType === "CONTENTS" || menuType === "BOARD") && (
|
||||
<TableRow>
|
||||
<TableHead>메뉴 평가 사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="evlUseYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="evlUseY" />
|
||||
<Label htmlFor="evlUseY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="evlUseN" />
|
||||
<Label htmlFor="evlUseN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
{/* 메뉴 유형 공통 폼 S */}
|
||||
<TableRow>
|
||||
<TableHead>메뉴 서브명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuSubKornNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트맵에서 다른 이름으로 사용할 경우 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>메뉴 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("iconNm") && "아이콘을 조회 후 선택하십시오."}
|
||||
{form.watch("iconNm")}
|
||||
</div>
|
||||
<IconFinder handleClick={handleIconClick} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>출력 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="otptYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="otptY" />
|
||||
<Label htmlFor="otptY">출력</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="otptN" />
|
||||
<Label htmlFor="otptN">출력안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* 메뉴 유형 공통 폼 E */}
|
||||
|
||||
{(menuType === "CONTENTS" || menuType === "BOARD") && (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableHead>QR코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="qrcd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
value={field.value ?? ""}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="qrcdY" />
|
||||
<Label htmlFor="qrcdY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="qrcdN" />
|
||||
<Label htmlFor="qrcdN">사용안함</Label>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>담당자</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-[3.2rem] w-[21.5rem] cursor-not-allowed rounded-md border border-input bg-form px-3 py-2 text-[1.3rem] opacity-50">
|
||||
{!form.watch("menuPicUserUnqIdNm") &&
|
||||
"메뉴 담당자를 조회 후 선택하십시오."}
|
||||
{form.watch("menuPicUserUnqIdNm")}
|
||||
</div>
|
||||
<OrganizationSearchDialog
|
||||
handleUserSelect={handleUserSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<b>지정일:</b>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="menuPicDsgnYmd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
mode="single"
|
||||
selected={
|
||||
field.value
|
||||
? dayjs(field.value).toDate()
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date);
|
||||
handleDateSelect(date);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export { TypeMenuForm };
|
||||
@@ -1,53 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
AuthorizationsService,
|
||||
AuthorizationsParams,
|
||||
AuthorizationsList,
|
||||
AuthorizationFormData,
|
||||
WithRoleAuthorizations,
|
||||
} from "../model/authorizations-interface";
|
||||
|
||||
export class AuthorizationsApiService implements AuthorizationsService {
|
||||
public async getAuthorizationsList(
|
||||
params: AuthorizationsParams,
|
||||
): Promise<AuthorizationsList> {
|
||||
const response = await client.get<AuthorizationsList>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getAuthorizationInfo(
|
||||
authorizationNo: string,
|
||||
): Promise<WithRoleAuthorizations> {
|
||||
const response = await client.get<WithRoleAuthorizations>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}/${authorizationNo}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createAuthorization(
|
||||
data: AuthorizationFormData,
|
||||
): Promise<WithRoleAuthorizations> {
|
||||
const response = await client.post<WithRoleAuthorizations>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateAuthorization(
|
||||
authorizationNo: string,
|
||||
data: AuthorizationFormData,
|
||||
): Promise<WithRoleAuthorizations> {
|
||||
const response = await client.put<WithRoleAuthorizations>(
|
||||
`${ENDPOINT.USER_SERVICE.AUTHORIZATIONS}/${authorizationNo}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const AuthorizationsApi = new AuthorizationsApiService();
|
||||
@@ -1,43 +0,0 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
RoleAuthorizationsService,
|
||||
RoleAuthorizationsParams,
|
||||
RoleAuthorizationsList,
|
||||
RoleAuthorizationsData,
|
||||
RoleAuthorizations,
|
||||
} from "../model/role-authorizations-interface";
|
||||
|
||||
export class RoleAuthorizationsApiService implements RoleAuthorizationsService {
|
||||
public async getRoleAuthorizationsList(
|
||||
params: RoleAuthorizationsParams,
|
||||
): Promise<RoleAuthorizationsList> {
|
||||
const response = await client.get<RoleAuthorizationsList>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS}/api`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createRoleAuthorizations(
|
||||
data: RoleAuthorizationsData,
|
||||
): Promise<RoleAuthorizations> {
|
||||
const response = await client.post<RoleAuthorizations>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateRoleAuthorizations(
|
||||
data: RoleAuthorizationsData,
|
||||
): Promise<RoleAuthorizations> {
|
||||
const response = await client.put<RoleAuthorizations>(
|
||||
`${ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const RoleAuthorizationsApi = new RoleAuthorizationsApiService();
|
||||
@@ -1,3 +0,0 @@
|
||||
export { ModulePage } from "./ui/module-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
@@ -1,62 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface AuthorizationsParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
authorizationName?: string;
|
||||
httpMethodCode?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface Authorizations {
|
||||
authorizationNo: number;
|
||||
authorizationName: string;
|
||||
urlPatternValue: string;
|
||||
httpMethodCode: string;
|
||||
sortSeq: number;
|
||||
useYn: string;
|
||||
mdfr: string;
|
||||
mdfcnDt: string;
|
||||
}
|
||||
|
||||
export interface RoleAuthorizations {
|
||||
roleId: string;
|
||||
authorizationNo: number;
|
||||
authorizationName: string;
|
||||
urlPatternValue: string;
|
||||
httpMethodCode: string;
|
||||
sortSeq: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WithRoleAuthorizations extends Authorizations {
|
||||
roleAuthorizations: RoleAuthorizations[];
|
||||
}
|
||||
|
||||
export const authorizationFormSchema = z.object({
|
||||
authorizationName: z.string(),
|
||||
urlPatternValue: z.string(),
|
||||
httpMethodCode: z.string(),
|
||||
sortSeq: z.number(),
|
||||
useYn: z.string(),
|
||||
});
|
||||
|
||||
export type AuthorizationsList = PageableList<Authorizations>;
|
||||
export type AuthorizationFormData = z.infer<typeof authorizationFormSchema>;
|
||||
|
||||
export interface AuthorizationsService {
|
||||
getAuthorizationsList: (
|
||||
params: AuthorizationsParams,
|
||||
) => Promise<AuthorizationsList>;
|
||||
getAuthorizationInfo: (
|
||||
authorizationNo: string,
|
||||
) => Promise<WithRoleAuthorizations>;
|
||||
createAuthorization: (
|
||||
data: AuthorizationFormData,
|
||||
) => Promise<WithRoleAuthorizations>;
|
||||
updateAuthorization: (
|
||||
authorizationNo: string,
|
||||
data: AuthorizationFormData,
|
||||
) => Promise<WithRoleAuthorizations>;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import type { Authorizations } from "./authorizations-interface";
|
||||
import { ManagementAuthorityDialog } from "../ui/management-authority-dialog";
|
||||
|
||||
export const columns: ColumnDef<Authorizations>[] = [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
cell: ({ row }) => row.index + 1,
|
||||
},
|
||||
{
|
||||
accessorKey: "authorizationName",
|
||||
header: "API명",
|
||||
},
|
||||
{
|
||||
accessorKey: "urlPatternValue",
|
||||
header: "API URL",
|
||||
},
|
||||
{
|
||||
accessorKey: "httpMethodCode",
|
||||
header: "요청 메소드",
|
||||
},
|
||||
{
|
||||
accessorKey: "useYn",
|
||||
header: "사용여부",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfr",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = dayjs(row.original.mdfcnDt);
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
header: "권한관리",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<ManagementAuthorityDialog
|
||||
authorizationNo={row.original.authorizationNo.toString()}
|
||||
rowData={row.original}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,45 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface RoleAuthorizationsParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
keyword?: string;
|
||||
authorizationNo?: string;
|
||||
}
|
||||
|
||||
export interface RoleAuthorizations {
|
||||
roleId: string;
|
||||
roleName: string;
|
||||
roleContent: string;
|
||||
useYn: string;
|
||||
authorizationNo: number;
|
||||
authYn: string;
|
||||
}
|
||||
|
||||
export const roleAuthorizationsSchema = z.object({
|
||||
roleId: z.string(),
|
||||
authorizationNo: z.number(),
|
||||
authYn: z.string(),
|
||||
});
|
||||
|
||||
export const roleAuthorizationsFormSchema = z.object({
|
||||
roleAuthorizations: z.array(roleAuthorizationsSchema),
|
||||
});
|
||||
|
||||
export type RoleAuthorizationsList = PageableList<RoleAuthorizations>;
|
||||
export type RoleAuthorizationsData = z.infer<typeof roleAuthorizationsSchema>[];
|
||||
export type RoleAuthorizationsFormData = z.infer<
|
||||
typeof roleAuthorizationsFormSchema
|
||||
>;
|
||||
|
||||
export interface RoleAuthorizationsService {
|
||||
getRoleAuthorizationsList: (
|
||||
params: RoleAuthorizationsParams,
|
||||
) => Promise<RoleAuthorizationsList>;
|
||||
createRoleAuthorizations: (
|
||||
data: RoleAuthorizationsData,
|
||||
) => Promise<RoleAuthorizations>;
|
||||
updateRoleAuthorizations: (
|
||||
data: RoleAuthorizationsData,
|
||||
) => Promise<RoleAuthorizations>;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { RoleAuthorizations } from "./role-authorizations-interface";
|
||||
import type { RoleAuthorizationsFormData } from "./role-authorizations-interface";
|
||||
|
||||
export const createColumns = (
|
||||
form: UseFormReturn<RoleAuthorizationsFormData>,
|
||||
): ColumnDef<RoleAuthorizations>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={form
|
||||
.watch("roleAuthorizations")
|
||||
.every((role) => role.authYn === "Y")}
|
||||
onCheckedChange={(checked) => {
|
||||
table.getRowModel().rows.forEach((row, index) => {
|
||||
form.setValue(
|
||||
`roleAuthorizations.${index}.authYn`,
|
||||
checked ? "Y" : "N",
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={form.watch(`roleAuthorizations.${row.index}.authYn`) === "Y"}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(
|
||||
`roleAuthorizations.${row.index}.authYn`,
|
||||
checked ? "Y" : "N",
|
||||
);
|
||||
}}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "roleId",
|
||||
header: "권한ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "roleName",
|
||||
header: "권한명",
|
||||
},
|
||||
{
|
||||
accessorKey: "roleContent",
|
||||
header: "권한설명",
|
||||
},
|
||||
];
|
||||
@@ -1,45 +0,0 @@
|
||||
"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 { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
authorizationFormSchema,
|
||||
type AuthorizationFormData,
|
||||
} from "../model/authorizations-interface";
|
||||
import { AuthorizationsApi } from "../api/authorization-service";
|
||||
import { ModuleForm } from "./module-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { mutateAsync: createAuthorization } = useMutation({
|
||||
mutationFn: (data: AuthorizationFormData) =>
|
||||
AuthorizationsApi.createAuthorization(data),
|
||||
onSuccess: () => {
|
||||
alert("등록되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<AuthorizationFormData>({
|
||||
resolver: zodResolver(authorizationFormSchema),
|
||||
defaultValues: {
|
||||
sortSeq: 0,
|
||||
useYn: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">모듈API 등록</h3>
|
||||
</div>
|
||||
<ModuleForm form={form} handleSave={createAuthorization} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -1,148 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@common/components/ui";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import {
|
||||
roleAuthorizationsFormSchema,
|
||||
type RoleAuthorizationsFormData,
|
||||
type RoleAuthorizationsData,
|
||||
} from "../model/role-authorizations-interface";
|
||||
import { createColumns } from "../model/role-authorizations-table-columns";
|
||||
import type { Authorizations } from "../model/authorizations-interface";
|
||||
import { RoleAuthorizationsApi } from "../api/role-authorization-service";
|
||||
|
||||
type ManagementAuthorityDialogProps = {
|
||||
authorizationNo?: string;
|
||||
rowData?: Authorizations;
|
||||
} & React.ComponentProps<typeof Dialog>;
|
||||
|
||||
function ManagementAuthorityDialog({
|
||||
authorizationNo,
|
||||
rowData,
|
||||
...props
|
||||
}: ManagementAuthorityDialogProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const { data: roleAuthorizationList } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.ROLE_AUTHORIZATIONS, authorizationNo],
|
||||
queryFn: () =>
|
||||
RoleAuthorizationsApi.getRoleAuthorizationsList({ authorizationNo }),
|
||||
enabled: !!authorizationNo && isOpened,
|
||||
});
|
||||
|
||||
const { mutateAsync: saveRoleAuthorization } = useMutation({
|
||||
mutationFn: (data: RoleAuthorizationsData) =>
|
||||
RoleAuthorizationsApi.createRoleAuthorizations(data),
|
||||
onSuccess: () => {
|
||||
alert("저장되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<RoleAuthorizationsFormData>({
|
||||
resolver: zodResolver(roleAuthorizationsFormSchema),
|
||||
values: {
|
||||
roleAuthorizations:
|
||||
roleAuthorizationList?.content.map(
|
||||
({ roleId, authorizationNo, useYn }) => ({
|
||||
roleId,
|
||||
authorizationNo,
|
||||
authYn: useYn,
|
||||
}),
|
||||
) ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: roleAuthorizationList?.content ?? [],
|
||||
columns: createColumns(form),
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await confirm("저장하시겠습니까?")) {
|
||||
const data = form.getValues("roleAuthorizations");
|
||||
await saveRoleAuthorization(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" onClick={() => setIsOpened(true)}>
|
||||
관리
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>모듈API 권한관리</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-8">
|
||||
<Table variant="line">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>API명</TableHead>
|
||||
<TableHead>요청 메소드</TableHead>
|
||||
<TableHead>API URL</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell align="center">
|
||||
{rowData?.authorizationName}
|
||||
</TableCell>
|
||||
<TableCell align="center">{rowData?.httpMethodCode}</TableCell>
|
||||
<TableCell align="center">{rowData?.urlPatternValue}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button size="lg" onClick={handleSave}>
|
||||
저장
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManagementAuthorityDialog };
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import {
|
||||
authorizationFormSchema,
|
||||
type AuthorizationFormData,
|
||||
} from "../model/authorizations-interface";
|
||||
import { AuthorizationsApi } from "../api/authorization-service";
|
||||
import { ModuleForm } from "./module-form";
|
||||
|
||||
function ModifyPage({ sequential }: { sequential: string }) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: authorizationInfo } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.AUTHORIZATIONS, sequential],
|
||||
queryFn: () => AuthorizationsApi.getAuthorizationInfo(sequential),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateAuthorization } = useMutation({
|
||||
mutationFn: (data: AuthorizationFormData) =>
|
||||
AuthorizationsApi.updateAuthorization(sequential, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<AuthorizationFormData>({
|
||||
resolver: zodResolver(authorizationFormSchema),
|
||||
values: authorizationInfo,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">모듈API 수정</h3>
|
||||
</div>
|
||||
<ModuleForm
|
||||
form={form}
|
||||
authorizationNo={Number(sequential)}
|
||||
handleSave={updateAuthorization}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -1,166 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import {
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormWrapper } from "@/widgets/form-wrapper";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import type {
|
||||
AuthorizationFormData,
|
||||
WithRoleAuthorizations,
|
||||
} from "../model/authorizations-interface";
|
||||
|
||||
type BannerFormProps = {
|
||||
form: UseFormReturn<AuthorizationFormData>;
|
||||
authorizationNo?: number;
|
||||
handleSave: (data: AuthorizationFormData) => Promise<WithRoleAuthorizations>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
|
||||
return (
|
||||
<FormWrapper form={form} isModifyMode={!!authorizationNo} {...props}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="15%" />
|
||||
<col width="85%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>API명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="authorizationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="API명을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>API URL</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="urlPatternValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="API URL을 입력해주세요"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>요청 메소드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="httpMethodCode"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="HTTPMHD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>정렬순서</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sortSeq"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
min={0}
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (isNaN(Number(value))) return;
|
||||
field.onChange(Number(value));
|
||||
}}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
placeholder="정렬순서 입력"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사용여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="useYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="useYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="useY" />
|
||||
<Label htmlFor="useY">사용</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="useN" />
|
||||
<Label htmlFor="useN">사용안함</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModuleForm };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user