iipbs_cs #14
@@ -0,0 +1,2 @@
|
||||
import { DashboardPage } from "@/pages/dashboard";
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,2 @@
|
||||
import { ImagePage } from "@/pages/image";
|
||||
export default ImagePage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage as VModifyPage } from "@/pages/image";
|
||||
|
||||
export default async function ModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ serial: string }>;
|
||||
}) {
|
||||
const { serial } = await params;
|
||||
return <VModifyPage serial={serial} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/image";
|
||||
export default CreatePage;
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
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,
|
||||
}: {
|
||||
@@ -9,10 +14,22 @@ export default function WithBaseLayout({
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseLayout currentPath={pathname ?? ""} routeHandler={router.push}>
|
||||
{children}
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ export function middleware(req: NextRequest) {
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
console.log("Rewriting to: ", newUrl);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
@@ -20,5 +24,5 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: "/server/:path*",
|
||||
matcher: ["/server/:path*", "/attach-service/:path*"],
|
||||
};
|
||||
|
||||
+1
-33
@@ -1,37 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
import webpack from "webpack";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
webpack: (config) => {
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
"import.meta.env.VITE_PORT": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PORT,
|
||||
),
|
||||
"import.meta.env.VITE_PROXY_HOST": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST,
|
||||
),
|
||||
"import.meta.env.VITE_SERVER_API_URL": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_SERVER_API_URL,
|
||||
),
|
||||
"import.meta.env.VITE_SITE_ID": JSON.stringify(process.env.SITE_ID),
|
||||
"import.meta.env.VITE_CLAIM_NAME": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_CLAIM_NAME,
|
||||
),
|
||||
"import.meta.env.VITE_ACCESS_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_ACCESS_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_REFRESH_TOKEN": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_REFRESH_TOKEN,
|
||||
),
|
||||
"import.meta.env.VITE_AUTH_USER_ID": JSON.stringify(
|
||||
process.env.NEXT_PUBLIC_AUTH_USER_ID,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
|
||||
import { AuthWrapper } from "@/entities/auth";
|
||||
import { MenuWrapper } from "@/entities/menu";
|
||||
import { StoreProvider } from "@/shared/providers";
|
||||
import { AlertDialog } from "@/shared/ui/alert-dialog";
|
||||
|
||||
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
{children}
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StoreProvider>
|
||||
<ConfirmProvider>
|
||||
<AuthWrapper>
|
||||
<MenuWrapper>{children}</MenuWrapper>
|
||||
<AlertDialog />
|
||||
</AuthWrapper>
|
||||
</ConfirmProvider>
|
||||
</StoreProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
/* 블랙 테마 */
|
||||
[data-theme="black"] {
|
||||
.card-border {
|
||||
border-color: #3f4042;
|
||||
background-color: rgba(255, 255, 255, 0.065);
|
||||
border-color: #38383a;
|
||||
}
|
||||
.card-header {
|
||||
border-bottom-color: #3f4042;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { LoginApi } from "./api/login-service";
|
||||
export { AuthWrapper } from "./ui/auth-wrapper";
|
||||
export { AuthWrapper } from "./lib/auth-wrapper";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { client } from "@common/components/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { MenuItem, MenuService } from "../model/menu-interface";
|
||||
|
||||
export class MenuApiService implements MenuService {
|
||||
async getMenuRoles(siteId: string): Promise<MenuItem[]> {
|
||||
const response = await client.get<MenuItem[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const MenuApi = new MenuApiService();
|
||||
@@ -0,0 +1 @@
|
||||
export { MenuWrapper } from "./lib/menu-wrapper";
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { MenuApi } from "../api/menu-service";
|
||||
import { getCurrentMenu } from "./utils";
|
||||
|
||||
const MenuWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const pathname = usePathname();
|
||||
const { setCurrentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const menuRoles = await MenuApi.getMenuRoles(SITE_ID);
|
||||
if (!menuRoles) return;
|
||||
|
||||
const currentMenu = getCurrentMenu(menuRoles, pathname ?? "");
|
||||
if (!currentMenu) return;
|
||||
|
||||
setCurrentMenuInfo(currentMenu);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { MenuWrapper };
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { MenuItem } from "../model/menu-interface";
|
||||
|
||||
export const getCurrentMenu = (
|
||||
menus: MenuItem[],
|
||||
pathname: string,
|
||||
): MenuItem | null => {
|
||||
for (const menu of menus) {
|
||||
if (menu.children) {
|
||||
return getCurrentMenu(menu.children, pathname);
|
||||
}
|
||||
if (menu.urlPath === pathname) return menu;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
export interface MenuService {
|
||||
getMenuRoles(siteId: string): Promise<MenuItem[]>;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class SiteApiService implements SiteService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifySite(siteId: string, data: SiteFormData): Promise<void> {
|
||||
public async updateSite(siteId: string, data: SiteFormData): Promise<void> {
|
||||
const validatedData = siteFormSchema.parse(data);
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface SiteDetail extends SiteFormData {
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdFrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const siteFormSchema = z.object({
|
||||
@@ -54,6 +54,6 @@ export type SiteList = PageableList<Site>;
|
||||
export interface SiteService {
|
||||
getSiteList(params: SiteParams): Promise<SiteList>;
|
||||
siteDetail(siteId: string): Promise<SiteDetail>;
|
||||
modifySite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
updateSite(siteId: string, data: SiteFormData): Promise<void>;
|
||||
createSite(data: SiteFormData): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import Image from "next/image";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
function LoginPage() {
|
||||
const router = useRouter();
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -16,7 +14,7 @@ function LoginPage() {
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
router.push("/site");
|
||||
window.location.href = "/dashboard";
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { DashboardPage } from "./ui/dashboard-page";
|
||||
@@ -0,0 +1,5 @@
|
||||
function DashboardPage() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
@@ -0,0 +1,47 @@
|
||||
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();
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { ModifyPage } from "./ui/modify-page";
|
||||
export { ImagePage } from "./ui/image-page";
|
||||
@@ -0,0 +1,61 @@
|
||||
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);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
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: string;
|
||||
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>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"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") as string;
|
||||
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: "사용여부",
|
||||
cell: ({ row }) => (row.getValue("useYn") === "Y" ? "사용" : "미사용"),
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfrNm",
|
||||
header: "최종수정자",
|
||||
},
|
||||
{
|
||||
accessorKey: "mdfcnDt",
|
||||
header: "최종수정일",
|
||||
cell: ({ row }) => {
|
||||
const date = row.getValue("mdfcnDt") as string;
|
||||
return date ? dayjs(date).format("YYYY-MM-DD") : "";
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,40 @@
|
||||
"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 };
|
||||
@@ -0,0 +1,317 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { HttpResponse, Upload } from "tus-js-client";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Button,
|
||||
FileUploader,
|
||||
Input,
|
||||
Label,
|
||||
Progress,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { handleUpload } from "../lib/utils";
|
||||
import type { ImageFormData } from "../model/image-interface";
|
||||
|
||||
type ImageFormProps = {
|
||||
form: UseFormReturn<ImageFormData>;
|
||||
imgSn?: string;
|
||||
handleSave: (data: ImageFormData) => Promise<void>;
|
||||
handleDelete?: () => Promise<void>;
|
||||
};
|
||||
|
||||
function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
|
||||
const isModifyMode = !!imgSn;
|
||||
const atflYn = form.watch("atflYn");
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
const [upload, setUpload] = useState<Upload | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: ImageFormData) => {
|
||||
const confirmMessage = isModifyMode
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
await handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
await handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleProgress = (percentage: string) => {
|
||||
setProgress(Number(percentage));
|
||||
};
|
||||
|
||||
const handleUploadResponse = (res: HttpResponse) => {
|
||||
const fileInfo = res.getHeader("X-File-Info");
|
||||
if (fileInfo === undefined || fileInfo === null) return;
|
||||
|
||||
form.setValue("atflCd", JSON.parse(fileInfo)?.atflCd);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</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}
|
||||
value={field.value ?? ""}
|
||||
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"
|
||||
onUpload={(e) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
handleUpload(files, {
|
||||
handleUploadStart: setUpload,
|
||||
handleProgress,
|
||||
handleSuccess: setUpload,
|
||||
handleResponse: handleUploadResponse,
|
||||
});
|
||||
}}
|
||||
customPreview={
|
||||
<Label htmlFor="atflCd">
|
||||
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
|
||||
<p className="absolute left-0 top-0 p-2 text-base leading-none">
|
||||
{(upload?.file as File)?.name}
|
||||
</p>
|
||||
<div></div>
|
||||
<Progress
|
||||
value={progress}
|
||||
className="absolute bottom-0 left-0"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
size="lg"
|
||||
color="white"
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" type="submit">
|
||||
저장
|
||||
</Button>
|
||||
{isModifyMode && (
|
||||
<Button size="lg" color="red" type="button" onClick={onDelete}>
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImageForm };
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type RowSelectionState,
|
||||
} from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { columns } from "../model/table-columns";
|
||||
import { type Image, type ImageParams } from "../model/image-interface";
|
||||
|
||||
function ImagePage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [imageQuery, setImageQuery] = useState<ImageParams>({
|
||||
keyword: "",
|
||||
imgTypeCd: "",
|
||||
useYn: "",
|
||||
page: 0,
|
||||
size: 100,
|
||||
});
|
||||
|
||||
const { data: imageList, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES],
|
||||
queryFn: () => ImageApi.getImageList(imageQuery),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: imageList?.content ?? [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
},
|
||||
enableMultiRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Image) => {
|
||||
router.push(`/image/register/${row.imgSn}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<h3 className="text-2xl font-medium text-label">이미지 목록</h3>
|
||||
<Button onClick={() => router.push("/image/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ImagePage };
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
|
||||
import { ImageApi } from "../api/image-service";
|
||||
import { ImageForm } from "./image-form";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
|
||||
function ModifyPage({ serial }: { serial: string }) {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const { data: imageDetail } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, serial],
|
||||
queryFn: () => ImageApi.getImageInfo(serial),
|
||||
});
|
||||
|
||||
const { mutateAsync: updateImage } = useMutation({
|
||||
mutationFn: (data: ImageFormData) => ImageApi.updateImage(serial, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteImage } = useMutation({
|
||||
mutationFn: () => ImageApi.deleteImage(serial),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<ImageFormData>({
|
||||
resolver: zodResolver(imageFormSchema),
|
||||
values: imageDetail,
|
||||
});
|
||||
|
||||
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}
|
||||
imgSn={serial}
|
||||
handleSave={updateImage}
|
||||
handleDelete={deleteImage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ModifyPage };
|
||||
@@ -24,7 +24,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenu(
|
||||
public async updateMenu(
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
): Promise<void> {
|
||||
@@ -49,7 +49,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
|
||||
data,
|
||||
@@ -57,7 +57,7 @@ export class MenuApiService implements MenuService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async modifyMenuName(menuCd: number, name: string): Promise<void> {
|
||||
public async updateMenuName(menuCd: number, name: string): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Checkbox } from "@common/components/ui";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import type { Content } from "./content-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<Content>();
|
||||
|
||||
@@ -75,12 +75,12 @@ export type Menu = Omit<MenuFormData, "iconNm"> & {
|
||||
export interface MenuService {
|
||||
getMenuInfo: (menuCd: number) => Promise<Menu>;
|
||||
createMenu: (data: MenuFormData) => Promise<void>;
|
||||
modifyMenu: (
|
||||
updateMenu: (
|
||||
menuCd: number,
|
||||
data: Omit<MenuFormData, "siteId">,
|
||||
) => Promise<void>;
|
||||
deleteMenu: (menuCd: number) => Promise<void>;
|
||||
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
|
||||
modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
|
||||
updateMenuName: (menuCd: number, name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -10,12 +10,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import { MENU_TYPE, type MenuFormData } from "../model/menu-interface";
|
||||
|
||||
interface BaseMenuFormProps {
|
||||
@@ -57,7 +52,6 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
placeholder="메뉴명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -79,7 +73,6 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
placeholder="메뉴 영문명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -118,7 +111,6 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ContentApi } from "../api/content-service";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import { columns } from "../model/table-colums";
|
||||
import { columns } from "../model/content-table-colums";
|
||||
|
||||
type ContentSearchDialogProps = {
|
||||
handleContentSelect: (content: Content) => void;
|
||||
|
||||
@@ -13,7 +13,7 @@ interface MenuFormProps {
|
||||
form: UseFormReturn<MenuFormData>;
|
||||
menuCd?: number;
|
||||
handleCreate: (data: MenuFormData) => void;
|
||||
handleModify: ({
|
||||
handleUpdate: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
@@ -27,7 +27,7 @@ function MenuForm({
|
||||
form,
|
||||
menuCd,
|
||||
handleCreate,
|
||||
handleModify,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
}: MenuFormProps) {
|
||||
const isModifyMode = !!menuCd;
|
||||
@@ -35,15 +35,15 @@ function MenuForm({
|
||||
const { confirm } = useConfirm();
|
||||
const [onConfirm, setOnConfirm] = useState(false);
|
||||
|
||||
const handleConfirm = async (fn: () => Promise<void>) => {
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await fn();
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: MenuFormData) => {
|
||||
if (isModifyMode) {
|
||||
await onModify(data);
|
||||
await onUpdate(data);
|
||||
} else {
|
||||
await onCreate(data);
|
||||
}
|
||||
@@ -57,10 +57,10 @@ function MenuForm({
|
||||
});
|
||||
};
|
||||
|
||||
const onModify = async (data: MenuFormData) => {
|
||||
const onUpdate = async (data: MenuFormData) => {
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("수정하시겠습니까?")) {
|
||||
handleModify({ menuCd: menuCd!, data });
|
||||
handleUpdate({ menuCd: menuCd!, data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,10 +34,10 @@ function MenuNode({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutateAsync: modifyMenuName } = useMutation({
|
||||
const { mutateAsync: updateMenuName } = useMutation({
|
||||
mutationFn: (nodeName: string) => {
|
||||
const encodedName = encodeURIComponent(nodeName);
|
||||
return MenuApi.modifyMenuName(id as number, encodedName);
|
||||
return MenuApi.updateMenuName(id as number, encodedName);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -53,7 +53,7 @@ function MenuNode({
|
||||
const handleSelect = () => onSelect(node);
|
||||
|
||||
const handleSave = async () => {
|
||||
await modifyMenuName(nodeName);
|
||||
await updateMenuName(nodeName);
|
||||
setEditMode(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from "@common/components/ui";
|
||||
import { SiteApi } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import type { TreeMenu } from "../model/menu-interface";
|
||||
@@ -81,14 +80,14 @@ function MenuPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: modifyMenu } = useMutation({
|
||||
const { mutate: updateMenu } = useMutation({
|
||||
mutationFn: ({
|
||||
menuCd,
|
||||
data,
|
||||
}: {
|
||||
menuCd: number;
|
||||
data: Omit<MenuFormData, "siteId">;
|
||||
}) => MenuApi.modifyMenu(menuCd, data),
|
||||
}) => MenuApi.updateMenu(menuCd, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
@@ -197,67 +196,65 @@ function MenuPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageLayout pageTitle="메뉴 관리">
|
||||
<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}
|
||||
handleModify={modifyMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
</PageLayout>
|
||||
<div className="card card-border flex-1">
|
||||
<MenuForm
|
||||
form={form}
|
||||
menuCd={selectedMenu?.menuCd}
|
||||
handleCreate={createMenu}
|
||||
handleUpdate={updateMenu}
|
||||
handleDelete={deleteMenu}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog open={onConfirm} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -41,9 +41,9 @@ function MenuTree({
|
||||
const queryClient = useQueryClient();
|
||||
const treeMethods = useRef<TreeMethods>(null);
|
||||
|
||||
const { mutate: modifyTreeMenu } = useMutation({
|
||||
const { mutate: updateTreeMenu } = useMutation({
|
||||
mutationFn: (newTree: TreeNode) =>
|
||||
MenuApi.modifyTreeMenu(siteId, [newTree]),
|
||||
MenuApi.updateTreeMenu(siteId, [newTree]),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
|
||||
@@ -96,7 +96,7 @@ function MenuTree({
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onDrop={handleDropNode(modifyTreeMenu)}
|
||||
onDrop={handleDropNode(updateTreeMenu)}
|
||||
classes={{
|
||||
root: "p-4",
|
||||
container: "flex flex-col",
|
||||
|
||||
@@ -13,12 +13,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
|
||||
import type { BoardPopup } from "@/entities/board";
|
||||
import type { Content } from "../model/content-interface";
|
||||
import type { User } from "../model/organization-interface";
|
||||
@@ -117,7 +112,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -153,7 +147,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -186,7 +179,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -211,7 +203,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
placeholder="사이트맵에서 다른 이름으로 사용할 경우 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -255,7 +246,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -287,7 +277,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -322,7 +311,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function CreatePage() {
|
||||
@@ -38,9 +37,12 @@ function CreatePage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 등록</h3>
|
||||
</div>
|
||||
<SiteForm form={form} handleSave={createSite} />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,14 +22,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { DomainApi } from "../api/domain-service";
|
||||
import {
|
||||
@@ -233,7 +228,6 @@ function DomainManagementDialog({
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -253,7 +247,6 @@ function DomainManagementDialog({
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -275,20 +268,19 @@ function DomainManagementDialog({
|
||||
value="Y"
|
||||
id={`useY-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useY-${i}`}>예</label>
|
||||
<Label htmlFor={`useY-${i}`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`useN-${i}`}
|
||||
/>
|
||||
<label htmlFor={`useN-${i}`}>
|
||||
<Label htmlFor={`useN-${i}`}>
|
||||
아니오
|
||||
</label>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -22,14 +22,9 @@ import {
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Checkbox,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { IpApi } from "../api/ip-service";
|
||||
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
|
||||
@@ -217,7 +212,6 @@ function IPManagementDialog({
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-left" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -239,20 +233,19 @@ function IPManagementDialog({
|
||||
value="Y"
|
||||
id={`prmY-${i}`}
|
||||
/>
|
||||
<label htmlFor={`prmY-${i}`}>예</label>
|
||||
<Label htmlFor={`prmY-${i}`}>예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="N"
|
||||
id={`prmN-${i}`}
|
||||
/>
|
||||
<label htmlFor={`prmN-${i}`}>
|
||||
<Label htmlFor={`prmN-${i}`}>
|
||||
아니오
|
||||
</label>
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -6,13 +6,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { SiteApi, siteFormSchema, type SiteFormData } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { useAlertStore } from "@/shared/lib/use-alert-store";
|
||||
import { SiteForm } from "./site-form";
|
||||
|
||||
function ModifyPage({ siteId }: { siteId: string }) {
|
||||
const isModifyMode = !!siteId;
|
||||
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
@@ -31,11 +28,11 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
bscSiteYn: data.basicSiteYn,
|
||||
};
|
||||
},
|
||||
enabled: isModifyMode,
|
||||
enabled: !!siteId,
|
||||
});
|
||||
|
||||
const { mutateAsync: modifySite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
|
||||
const { mutateAsync: updateSite } = useMutation({
|
||||
mutationFn: (data: SiteFormData) => SiteApi.updateSite(siteId, data),
|
||||
onSuccess: () => {
|
||||
alert("수정되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
@@ -48,12 +45,7 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
const { mutateAsync: deleteSite } = useMutation({
|
||||
mutationFn: () => SiteApi.deleteSite(siteId),
|
||||
onSuccess: () => {
|
||||
alert("삭제되었습니다.", () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.SITES],
|
||||
});
|
||||
router.back();
|
||||
});
|
||||
alert("삭제되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,14 +55,17 @@ function ModifyPage({ siteId }: { siteId: string }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<div className="card card-border !mb-0 !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 수정</h3>
|
||||
</div>
|
||||
<SiteForm
|
||||
form={form}
|
||||
siteId={siteId}
|
||||
handleSave={modifySite}
|
||||
handleSave={updateSite}
|
||||
handleDelete={deleteSite}
|
||||
/>
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,7 @@ import {
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { SiteFormData } from "@/entities/site";
|
||||
import {
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormField,
|
||||
Form,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
|
||||
import type { Domain } from "../model/domain-interface";
|
||||
import { IPManagementDialog } from "./ip-management-dialog";
|
||||
@@ -44,6 +38,12 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
|
||||
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
|
||||
|
||||
const handleConfirm = async (callback: () => Promise<void>) => {
|
||||
setOnConfirm(true);
|
||||
await callback();
|
||||
setOnConfirm(false);
|
||||
};
|
||||
|
||||
const handleSaveDomain = (rows: Domain[]) => {
|
||||
form.setValue(
|
||||
"dmnAddr",
|
||||
@@ -56,350 +56,329 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
|
||||
? "수정하시겠습니까?"
|
||||
: "저장하시겠습니까?";
|
||||
|
||||
setOnConfirm(true);
|
||||
if (await confirm(confirmMessage)) {
|
||||
setOnConfirm(false);
|
||||
handleSave(data);
|
||||
}
|
||||
setOnConfirm(false);
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm(confirmMessage)) {
|
||||
handleSave(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
setOnConfirm(true);
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
setOnConfirm(false);
|
||||
handleDelete?.();
|
||||
}
|
||||
setOnConfirm(false);
|
||||
await handleConfirm(async () => {
|
||||
if (await confirm("삭제하시겠습니까?")) {
|
||||
handleDelete?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">
|
||||
사이트 등록/수정
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="card">
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>사이트 코드</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-[12rem] !text-[1.3rem]"
|
||||
disabled={isModifyMode}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 구분</TableHead>
|
||||
<TableCell className="border">
|
||||
<div className="flex gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteKndCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
name="siteKndCd"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
upCd="SITEKNDCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<IPManagementDialog
|
||||
siteId={siteId}
|
||||
triggerDisabled={form.watch("siteKndCd") === "O"}
|
||||
open={isIpManagementOpen}
|
||||
onOpenChange={setIsIpManagementOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 디자인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteSkn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CodeSelect
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
upCd="SITESKINCD"
|
||||
onValueChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
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="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<TableRow>
|
||||
<TableHead>사이트명</TableHead>
|
||||
<TableHead>사이트 도메인</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 설명</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteExpln"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 설명을 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isModifyMode && (
|
||||
<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("dmnAddr")}
|
||||
</div>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
<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("dmnAddr")}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 주소를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DomainManagementDialog
|
||||
siteId={siteId}
|
||||
open={isDomainManagementOpen}
|
||||
onOpenChange={setIsDomainManagementOpen}
|
||||
handleSaveDomain={handleSaveDomain}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 전화번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 전화번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 팩스번호</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="faxNo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 하단 팩스번호를 입력하십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 내용</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="아이콘 파일 경로 및 파일명을 입력하여 주십시오."
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기본 사이트 여부</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
<TableRow>
|
||||
<TableHead>사이트 하단 표기 주소</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteAddr"
|
||||
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="telNo"
|
||||
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="faxNo"
|
||||
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="lwndCn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="사이트 표기 내용을 입력하십시오. (ex. COPYRIGHT c 2013 KECO. ALL RIGHTS RESERVED.)"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>북마크 아이콘</TableHead>
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bkmkIcon"
|
||||
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="bscSiteYn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
{...field}
|
||||
name="bscSiteYn"
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="Y" id="bscSiteY" />
|
||||
<Label htmlFor="bscSiteY">예</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="N" id="bscSiteN" />
|
||||
<Label htmlFor="bscSiteN">아니오</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}
|
||||
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>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button
|
||||
|
||||
@@ -6,10 +6,9 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { Button, DataTable, Input } from "@common/components";
|
||||
import { Button, DataTable, Input, Label } from "@common/components";
|
||||
import { SiteApi, type Site, type SiteParams } from "@/entities/site";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
function SitePage() {
|
||||
@@ -44,34 +43,32 @@ function SitePage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout pageTitle="사이트 관리">
|
||||
<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={siteQuery.siteNm}
|
||||
onChange={(e) => setSiteQuery({ siteNm: e.target.value })}
|
||||
/>
|
||||
<Button className="h-[3.2rem]" type="submit">
|
||||
조회
|
||||
</Button>
|
||||
</form>
|
||||
<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={siteQuery.siteNm}
|
||||
onChange={(e) => setSiteQuery({ siteNm: 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("/site/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card card-border">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">사이트 목록</h3>
|
||||
<Button onClick={() => router.push("/site/register")}>등록</Button>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
</div>
|
||||
<div className="card !m-[1.2rem] h-[49rem] overflow-auto !p-0">
|
||||
<DataTable table={table} onRowClick={handleRowClick} />
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ import { PROXY_HOST } from "./env";
|
||||
export const BASE_URL = `${PROXY_HOST}/server`;
|
||||
|
||||
export const ENDPOINT = {
|
||||
ATTACH_SERVICE: {
|
||||
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
USERS: "/user-service/api/v1/users",
|
||||
@@ -14,8 +17,10 @@ export const ENDPOINT = {
|
||||
IPS: "/cms-service/api/v1/site-ips",
|
||||
DOMAINS: "/cms-service/api/v1/site-dmns",
|
||||
MENUS: "/cms-service/api/v1/menus",
|
||||
MENU_ROLES: "/cms-service/api/v1/menu-roles",
|
||||
CONTENTS: "/cms-service/api/v1/contents",
|
||||
USER_OGNZ: "/cms-service/api/v1/user-ognz",
|
||||
IMAGES: "/cms-service/api/v1/images",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { getCookie, setCookie, removeCookie } from "./cookie";
|
||||
export { useMenuStore } from "./use-menu-store";
|
||||
export { useUserStore } from "./use-user-store";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useStore } from "zustand";
|
||||
import { useContext } from "react";
|
||||
import type { MenuStore } from "../store/menu-store";
|
||||
import { StoreContext } from "../providers/store-provider";
|
||||
|
||||
export const useMenuStore = <T>(selector: (store: MenuStore) => T): T => {
|
||||
const storeContext = useContext(StoreContext);
|
||||
|
||||
if (!storeContext) {
|
||||
throw new Error("useMenuStore must be used within StoreProvider");
|
||||
}
|
||||
|
||||
return useStore(storeContext.menuStore, selector);
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useRef } from "react";
|
||||
import { createAlertStore } from "../store/alert-store";
|
||||
import { createUserStore } from "../store/user-store";
|
||||
import { createAlertStore, createMenuStore, createUserStore } from "../store";
|
||||
|
||||
type StoreContextType = {
|
||||
alertStore: ReturnType<typeof createAlertStore>;
|
||||
menuStore: ReturnType<typeof createMenuStore>;
|
||||
userStore: ReturnType<typeof createUserStore>;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
if (storeRef.current === null) {
|
||||
storeRef.current = {
|
||||
alertStore: createAlertStore(),
|
||||
menuStore: createMenuStore(),
|
||||
userStore: createUserStore(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { createAlertStore } from "./alert-store";
|
||||
export { createMenuStore } from "./menu-store";
|
||||
export { createUserStore, type IUserInfo } from "./user-store";
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import type { MenuItem } from "@common/components/api";
|
||||
|
||||
export type MenuState = {
|
||||
currentMenuInfo: MenuItem | null;
|
||||
};
|
||||
|
||||
export type MenuActions = {
|
||||
setCurrentMenuInfo: (menu: MenuItem) => void;
|
||||
};
|
||||
|
||||
export type MenuStore = MenuState & MenuActions;
|
||||
|
||||
export const defaultInitState: MenuState = {
|
||||
currentMenuInfo: null,
|
||||
};
|
||||
|
||||
export const createMenuStore = (initState: MenuState = defaultInitState) => {
|
||||
return createStore<MenuStore>()((set) => ({
|
||||
...initState,
|
||||
setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }),
|
||||
}));
|
||||
};
|
||||
@@ -1,15 +1,19 @@
|
||||
function PageLayout({
|
||||
children,
|
||||
pageTitle,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
pageTitle: string;
|
||||
}) {
|
||||
import { PageTitle } from "@common/components";
|
||||
import { useMenuStore } from "../lib/use-menu-store";
|
||||
|
||||
function PageLayout({ children }: { children: React.ReactNode }) {
|
||||
const { currentMenuInfo } = useMenuStore((state) => state);
|
||||
|
||||
const menuPaths = currentMenuInfo?.menuPath
|
||||
?.split(">")
|
||||
.map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container">
|
||||
<h2 className="px-[1.8rem] pt-[2rem] text-[2.4rem] font-semibold text-title">
|
||||
{pageTitle}
|
||||
</h2>
|
||||
<PageTitle
|
||||
title={currentMenuInfo?.menuKornNm ?? ""}
|
||||
menuPaths={menuPaths ?? []}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,15 +16,38 @@ const config: StorybookConfig = {
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
config.define = {
|
||||
"process.env.BASE_ASSETS_PATH": JSON.stringify(
|
||||
process.env.BASE_ASSETS_PATH,
|
||||
),
|
||||
"process.env": {
|
||||
BASE_ASSETS_PATH: JSON.stringify(process.env.BASE_ASSETS_PATH),
|
||||
PORT: JSON.stringify(process.env.PORT || "3000"),
|
||||
PROXY_HOST: JSON.stringify(
|
||||
process.env.PROXY_HOST || "http://localhost:3000",
|
||||
),
|
||||
SERVER_API_URL: JSON.stringify(process.env.SERVER_API_URL),
|
||||
SITE_ID: JSON.stringify(process.env.SITE_ID || "CS"),
|
||||
CLAIM_NAME: JSON.stringify(process.env.CLAIM_NAME || "Authorization"),
|
||||
ACCESS_TOKEN: JSON.stringify(
|
||||
process.env.ACCESS_TOKEN || "access-token",
|
||||
),
|
||||
REFRESH_TOKEN: JSON.stringify(
|
||||
process.env.REFRESH_TOKEN || "refresh-token",
|
||||
),
|
||||
AUTH_USER_ID: JSON.stringify(process.env.AUTH_USER_ID || "token-id"),
|
||||
},
|
||||
};
|
||||
config.plugins?.push(
|
||||
tsconfigPaths({
|
||||
projects: [path.resolve(path.dirname(__dirname), "tsconfig.json")],
|
||||
}),
|
||||
);
|
||||
config.server = {
|
||||
proxy: {
|
||||
"/server": {
|
||||
target: process.env.SERVER_API_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/server/, ""),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.2",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
|
||||
@@ -13,9 +13,11 @@ export interface MenuItem {
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface SiteInfo {
|
||||
mdfcnDt: string;
|
||||
mdFr: string;
|
||||
rgTrNm: string;
|
||||
mdFrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
export const getSiteInfo = async (siteId: string) => {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
export const PORT = import.meta.env.VITE_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
import.meta.env.VITE_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const PORT = process.env.PORT || "3000";
|
||||
export const PROXY_HOST = process.env.PROXY_HOST || `http://localhost:${PORT}`;
|
||||
|
||||
export const SERVER_API_URL = import.meta.env.VITE_SERVER_API_URL;
|
||||
export const SITE_ID = import.meta.env.VITE_SITE_ID || "CS";
|
||||
export const SERVER_API_URL = process.env.SERVER_API_URL;
|
||||
export const SITE_ID = process.env.SITE_ID || "CS";
|
||||
|
||||
export const CLAIM_NAME = import.meta.env.VITE_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN = import.meta.env.VITE_ACCESS_TOKEN || "access-token";
|
||||
export const REFRESH_TOKEN =
|
||||
import.meta.env.VITE_REFRESH_TOKEN || "refresh-token";
|
||||
export const AUTH_USER_ID = import.meta.env.VITE_AUTH_USER_ID || "token-id";
|
||||
export const CLAIM_NAME = process.env.CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN = process.env.ACCESS_TOKEN || "access-token";
|
||||
export const REFRESH_TOKEN = process.env.REFRESH_TOKEN || "refresh-token";
|
||||
export const AUTH_USER_ID = process.env.AUTH_USER_ID || "token-id";
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./utils";
|
||||
export * from "./cookie";
|
||||
export * from "./utils";
|
||||
export * from "./use-mobile";
|
||||
|
||||
@@ -2,13 +2,17 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
export function QueryProvider({
|
||||
children,
|
||||
client,
|
||||
queryClient,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
client?: QueryClient;
|
||||
queryClient?: QueryClient;
|
||||
}) {
|
||||
const queryClient = client ?? new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
if (!queryClient) {
|
||||
const queryClient = new QueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type RowData,
|
||||
type Table as TableType,
|
||||
} from "@tanstack/react-table";
|
||||
import { cn } from "../../lib";
|
||||
import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
@@ -38,19 +39,24 @@ function THead<T>({ table }: { table: TableType<T> }) {
|
||||
|
||||
type TableProps<T> = {
|
||||
table: TableType<T>;
|
||||
variant?: "default" | "secondary" | "line";
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
function DataTable<T>({ table, onRowClick }: TableProps<T>) {
|
||||
function DataTable<T>({
|
||||
table,
|
||||
variant = "default",
|
||||
onRowClick,
|
||||
}: TableProps<T>) {
|
||||
return (
|
||||
<Table className="w-full">
|
||||
<Table className="w-full" variant={variant}>
|
||||
<THead table={table} />
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={onRowClick ? "cursor-pointer hover:bg-muted" : ""}
|
||||
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const { meta } = cell.column.columnDef;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.file-search-container {
|
||||
@apply flex w-full max-w-sm items-center gap-2;
|
||||
}
|
||||
.file-search-preview {
|
||||
@apply h-12 w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
|
||||
}
|
||||
.file-search-label {
|
||||
@apply flex-shrink-0 cursor-pointer rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FileSearch } from ".";
|
||||
|
||||
const meta: Meta<typeof FileSearch> = {
|
||||
title: "UI/FileSearch",
|
||||
component: FileSearch,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
- [Docs](https://ui.shadcn.com/docs/components/input#file)
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FileSearch>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <FileSearch id="file-search" />,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import "./file-search.css";
|
||||
|
||||
import { Input } from "../input";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { combineFileName, validFileType } from "./utils";
|
||||
|
||||
type FileSearchProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
id: string;
|
||||
files?: ReadonlyArray<File>;
|
||||
};
|
||||
|
||||
export function FileSearch({
|
||||
id,
|
||||
className,
|
||||
files,
|
||||
...props
|
||||
}: FileSearchProps) {
|
||||
return (
|
||||
<div className={cn("file-search-container", className)}>
|
||||
<Input id={id} type="file" className="sr-only" {...props} />
|
||||
<div className="file-search-preview">
|
||||
{files?.filter(validFileType).map(combineFileName).join(", ")}
|
||||
</div>
|
||||
<label htmlFor={id} className="file-search-label">
|
||||
파일 선택
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.file-uploader-container {
|
||||
@apply flex h-[3.2rem] w-full max-w-sm items-center gap-2;
|
||||
}
|
||||
.file-uploader-preview {
|
||||
@apply h-full w-full min-w-56 overflow-x-auto whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-[1.2rem];
|
||||
}
|
||||
.file-uploader-label {
|
||||
@apply inline-flex h-full flex-shrink-0 cursor-pointer items-center rounded-sm border border-input bg-muted px-4 py-2 text-[1.2rem] font-medium text-muted-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FileUploader } from ".";
|
||||
|
||||
const meta: Meta<typeof FileUploader> = {
|
||||
title: "UI/FileUploader",
|
||||
component: FileUploader,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FileUploader>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <FileUploader id="file-uploader" onUpload={() => {}} />,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import "./file-uploader.css";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Input } from "../input";
|
||||
import { Label } from "../label";
|
||||
import { combineFileName, validFileType } from "./utils";
|
||||
|
||||
type FileUploaderProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
id: string;
|
||||
files?: ReadonlyArray<File>;
|
||||
customPreview?: React.ReactNode;
|
||||
onUpload: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
export function FileUploader({
|
||||
id,
|
||||
files,
|
||||
customPreview,
|
||||
className,
|
||||
onUpload,
|
||||
...props
|
||||
}: FileUploaderProps) {
|
||||
return (
|
||||
<div className={cn("file-uploader-container", className)}>
|
||||
<Input
|
||||
id={id}
|
||||
type="file"
|
||||
className="sr-only"
|
||||
{...props}
|
||||
onChange={onUpload}
|
||||
/>
|
||||
{customPreview || (
|
||||
<div className="file-uploader-preview">
|
||||
{files?.filter(validFileType).map(combineFileName).join(", ")}
|
||||
</div>
|
||||
)}
|
||||
<Label htmlFor={id} className="file-uploader-label">
|
||||
파일 선택
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,13 +7,15 @@ export * from "./data-table";
|
||||
export * from "./date-picker";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
export * from "./file-search";
|
||||
export * from "./file-uploader";
|
||||
export * from "./icon-finder";
|
||||
export * from "./input";
|
||||
export * from "./label";
|
||||
export * from "./layout";
|
||||
export * from "./loading";
|
||||
export * from "./page-title";
|
||||
export * from "./popover";
|
||||
export * from "./progress";
|
||||
export * from "./radio-group";
|
||||
export * from "./select";
|
||||
export * from "./slider";
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import type { MenuItem } from "../../api";
|
||||
import { QueryProvider } from "../../provider/query-provider";
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import { LNB } from "../lnb";
|
||||
import { Header } from "../header";
|
||||
import { Footer } from "./footer";
|
||||
|
||||
import { SidebarProvider } from "../sidebar";
|
||||
import "./common.css";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export function BaseLayout({
|
||||
children,
|
||||
queryClient,
|
||||
currentPath,
|
||||
routeHandler,
|
||||
handleMenuClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
currentPath?: string;
|
||||
routeHandler?: (path: string) => void;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<QueryProvider client={queryClient}>
|
||||
<QueryProvider queryClient={queryClient}>
|
||||
<SidebarProvider>
|
||||
<div className="flex-1">
|
||||
<Header />
|
||||
<div className="flex h-[calc(100vh-50px)]">
|
||||
<LNB currentPath={currentPath} routeHandler={routeHandler} />
|
||||
<LNB currentPath={currentPath} handleMenuClick={handleMenuClick} />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="max-h-[calc(100%-50px)] flex-1 overflow-auto">
|
||||
{children}
|
||||
|
||||
@@ -22,11 +22,11 @@ import { getParentIds } from "./lib";
|
||||
export function LNB({
|
||||
viewOnly,
|
||||
currentPath,
|
||||
routeHandler,
|
||||
handleMenuClick,
|
||||
}: {
|
||||
viewOnly?: boolean;
|
||||
currentPath?: string;
|
||||
routeHandler?: (path: string) => void;
|
||||
handleMenuClick?: (item: MenuItem) => void;
|
||||
}) {
|
||||
const { data: menuList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
@@ -37,7 +37,7 @@ export function LNB({
|
||||
const [openMenuIds, setOpenMenuIds] = useState<number[]>([]);
|
||||
|
||||
function isPathMatched(path: string) {
|
||||
return currentPath === path || currentPath?.startsWith(`${path}/`);
|
||||
return currentPath === path;
|
||||
}
|
||||
|
||||
function MenuItem({ item }: { item: MenuItem }) {
|
||||
@@ -45,7 +45,7 @@ export function LNB({
|
||||
|
||||
const handleClick = () => {
|
||||
setCookie("menu-code-key", item.menuCd.toString());
|
||||
routeHandler?.(item.urlPath);
|
||||
handleMenuClick?.(item);
|
||||
};
|
||||
|
||||
if (item.children) {
|
||||
@@ -57,7 +57,7 @@ export function LNB({
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"h-auto py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
|
||||
isMatched && "bg-accent text-primary",
|
||||
isMatched && "bg-accent !text-primary",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
@@ -90,15 +90,15 @@ export function LNB({
|
||||
<Collapsible open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="group flex items-center justify-between rounded-sm py-4 pr-2 hover:bg-accent data-[state=open]:text-primary">
|
||||
<div className="group flex items-center justify-between rounded-sm py-4 pr-2 hover:bg-accent">
|
||||
<SidebarMenuButton className="group/menu-button text-[1.4rem] group-data-[state=open]:bg-transparent">
|
||||
<i
|
||||
className={cn(
|
||||
"diveicon ml-2 text-label group-data-[state=open]:text-primary",
|
||||
"diveicon ml-2 text-label",
|
||||
`di-${item.iconNm}`,
|
||||
)}
|
||||
/>
|
||||
<span className="text-label group-hover/menu-button:text-foreground group-data-[state=open]:text-primary">
|
||||
<span className="text-label group-hover/menu-button:text-foreground">
|
||||
{item.menuKornNm}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
const PageTitle = ({
|
||||
title,
|
||||
menuPaths,
|
||||
}: {
|
||||
title: string;
|
||||
menuPaths: string[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between px-[1.8rem] pt-[2rem]">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title">{title}</h2>
|
||||
{menuPaths.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="diveicon di-home-fill text-foreground" />
|
||||
{menuPaths?.map((path, i) => (
|
||||
<React.Fragment key={`${i}-${path}`}>
|
||||
{i !== 0 && (
|
||||
<i className="diveicon di-arrow-forward text-foreground" />
|
||||
)}
|
||||
<span className="text-xl text-foreground">{path}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { PageTitle };
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PageTitle } from ".";
|
||||
|
||||
const meta: Meta<typeof PageTitle> = {
|
||||
title: "UI/PageTitle",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof PageTitle>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className="w-[40rem]">
|
||||
<PageTitle title="타이틀" menuPaths={["메뉴 1", "메뉴 2", "메뉴 3"]} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
||||
@@ -3,8 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "../../lib/use-mobile";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { cn, useIsMobile } from "../../lib";
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../input";
|
||||
import { Separator } from "../separator";
|
||||
|
||||
@@ -9,7 +9,7 @@ interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
|
||||
}
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
({ className, variant = "default", ...props }, ref) => (
|
||||
<div className="relative w-full">
|
||||
<table
|
||||
ref={ref}
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.table-ui tr {
|
||||
@apply border-0;
|
||||
}
|
||||
.table-ui th {
|
||||
@apply bg-[#5987bf] px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2] text-[#fff];
|
||||
@apply px-4 py-[1.2rem] text-[1.3rem] font-medium leading-[1.2];
|
||||
}
|
||||
.table-ui td {
|
||||
@apply border-b border-b-border px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2] text-label;
|
||||
@apply px-2 py-[1.2rem] text-[1.3rem] font-normal leading-[1.2];
|
||||
}
|
||||
|
||||
.table-ui.default th {
|
||||
@apply bg-[#5987bf] text-[#fff];
|
||||
}
|
||||
.table-ui.default td {
|
||||
@apply border-b border-b-border text-label;
|
||||
}
|
||||
.table-ui.secondary th {
|
||||
@apply border border-border bg-[#eaf3fb] text-[#647784];
|
||||
}
|
||||
|
||||
.table-ui.secondary td {
|
||||
@apply border border-border;
|
||||
}
|
||||
|
||||
.table-ui.line th {
|
||||
@apply border-0 border-b-2 border-t border-b-[#d4e0ea] border-t-[#d4e0ea] bg-[#fff] text-[#555];
|
||||
.table-ui.line tr {
|
||||
@apply border-b border-t border-[#d4e0ea] bg-transparent;
|
||||
}
|
||||
|
||||
.table-ui.line td {
|
||||
@apply border-b border-b-border;
|
||||
.table-ui.line th {
|
||||
@apply border-b-2 border-[#d4e0ea] text-[#555];
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.table-ui th {
|
||||
@apply bg-[#5e7b9c] text-[#fff];
|
||||
.table-ui.default tr {
|
||||
@apply bg-[#545556];
|
||||
}
|
||||
.table-ui.default th {
|
||||
@apply bg-[#62686d];
|
||||
}
|
||||
.table-ui.secondary tr {
|
||||
@apply bg-[#565b5f];
|
||||
}
|
||||
.table-ui.secondary th {
|
||||
@apply bg-[#1a1c1d];
|
||||
@apply bg-[#5e7b9c] text-[#fff];
|
||||
}
|
||||
.table-ui.line th {
|
||||
@apply bg-[#0a0a0a];
|
||||
@apply text-[#fff];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+139
-7
@@ -93,6 +93,9 @@ importers:
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
tus-js-client:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.24.2
|
||||
@@ -387,6 +390,9 @@ importers:
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@radix-ui/react-radio-group':
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -1620,7 +1626,7 @@ packages:
|
||||
magic-string: 0.27.0
|
||||
react-docgen-typescript: 2.2.2(typescript@5.6.3)
|
||||
typescript: 5.6.3
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
dev: true
|
||||
|
||||
/@jridgewell/gen-mapping@0.3.8:
|
||||
@@ -2292,6 +2298,27 @@ packages:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-progress@1.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.3.18
|
||||
'@types/react-dom': ^18.3.5
|
||||
react: ^18.3.1
|
||||
react-dom: ^18.3.1
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.1(@types/react@18.3.18)(react@18.3.1)
|
||||
'@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==}
|
||||
peerDependencies:
|
||||
@@ -3056,7 +3083,7 @@ packages:
|
||||
browser-assert: 1.2.1
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
dev: true
|
||||
|
||||
/@storybook/components@8.6.0(storybook@8.6.0):
|
||||
@@ -3188,7 +3215,7 @@ packages:
|
||||
resolve: 1.22.10
|
||||
storybook: 8.6.0(prettier@3.5.2)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
@@ -3634,7 +3661,7 @@ packages:
|
||||
'@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.9)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.2
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -4175,7 +4202,6 @@ packages:
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
dev: true
|
||||
|
||||
/bundle-require@5.1.0(esbuild@0.24.2):
|
||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||
@@ -4353,6 +4379,13 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/combine-errors@3.0.3:
|
||||
resolution: {integrity: sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==}
|
||||
dependencies:
|
||||
custom-error-instance: 2.1.1
|
||||
lodash.uniqby: 4.5.0
|
||||
dev: false
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -4422,6 +4455,10 @@ packages:
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
/custom-error-instance@2.1.1:
|
||||
resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==}
|
||||
dev: false
|
||||
|
||||
/d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -5715,7 +5752,6 @@ packages:
|
||||
|
||||
/graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
dev: true
|
||||
|
||||
/graphemer@1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
@@ -5993,6 +6029,11 @@ packages:
|
||||
call-bound: 1.0.3
|
||||
dev: true
|
||||
|
||||
/is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/is-string@1.1.1:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6097,6 +6138,10 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/js-base64@3.7.7:
|
||||
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||
dev: false
|
||||
|
||||
/js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -6219,6 +6264,37 @@ packages:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
dev: true
|
||||
|
||||
/lodash._baseiteratee@4.7.0:
|
||||
resolution: {integrity: sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==}
|
||||
dependencies:
|
||||
lodash._stringtopath: 4.8.0
|
||||
dev: false
|
||||
|
||||
/lodash._basetostring@4.12.0:
|
||||
resolution: {integrity: sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==}
|
||||
dev: false
|
||||
|
||||
/lodash._baseuniq@4.6.0:
|
||||
resolution: {integrity: sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==}
|
||||
dependencies:
|
||||
lodash._createset: 4.0.3
|
||||
lodash._root: 3.0.1
|
||||
dev: false
|
||||
|
||||
/lodash._createset@4.0.3:
|
||||
resolution: {integrity: sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==}
|
||||
dev: false
|
||||
|
||||
/lodash._root@3.0.1:
|
||||
resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==}
|
||||
dev: false
|
||||
|
||||
/lodash._stringtopath@4.8.0:
|
||||
resolution: {integrity: sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==}
|
||||
dependencies:
|
||||
lodash._basetostring: 4.12.0
|
||||
dev: false
|
||||
|
||||
/lodash.add@3.7.0:
|
||||
resolution: {integrity: sha512-/uxUZkB6vYiPoGSXADJJE+Ee4W2vU+bxZOFdvJdx+mDgv+qyAO76sWD84Q4FxsMgTjC1+gVFC9YNhdG243QRDw==}
|
||||
dev: false
|
||||
@@ -6248,6 +6324,17 @@ packages:
|
||||
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
||||
dev: true
|
||||
|
||||
/lodash.throttle@4.1.1:
|
||||
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
|
||||
dev: false
|
||||
|
||||
/lodash.uniqby@4.5.0:
|
||||
resolution: {integrity: sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==}
|
||||
dependencies:
|
||||
lodash._baseiteratee: 4.7.0
|
||||
lodash._baseuniq: 4.6.0
|
||||
dev: false
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
@@ -6905,6 +6992,14 @@ packages:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
/proper-lockfile@4.1.2:
|
||||
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
retry: 0.12.0
|
||||
signal-exit: 3.0.7
|
||||
dev: false
|
||||
|
||||
/proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
@@ -6914,6 +7009,10 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/querystringify@2.2.0:
|
||||
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||
dev: false
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -7272,6 +7371,10 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: false
|
||||
|
||||
/resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -7304,6 +7407,11 @@ packages:
|
||||
supports-preserve-symlinks-flag: 1.0.0
|
||||
dev: true
|
||||
|
||||
/retry@0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: false
|
||||
|
||||
/reusify@1.1.0:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
@@ -7517,6 +7625,10 @@ packages:
|
||||
side-channel-weakmap: 1.0.2
|
||||
dev: true
|
||||
|
||||
/signal-exit@3.0.7:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: false
|
||||
|
||||
/signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -8089,6 +8201,19 @@ packages:
|
||||
turbo-windows-arm64: 2.4.2
|
||||
dev: true
|
||||
|
||||
/tus-js-client@4.3.1:
|
||||
resolution: {integrity: sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==}
|
||||
engines: {node: '>=18'}
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
combine-errors: 3.0.3
|
||||
is-stream: 2.0.1
|
||||
js-base64: 3.7.7
|
||||
lodash.throttle: 4.1.1
|
||||
proper-lockfile: 4.1.2
|
||||
url-parse: 1.5.10
|
||||
dev: false
|
||||
|
||||
/type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8221,6 +8346,13 @@ packages:
|
||||
punycode: 2.3.1
|
||||
dev: true
|
||||
|
||||
/url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
requires-port: 1.0.0
|
||||
dev: false
|
||||
|
||||
/use-callback-ref@1.3.3(@types/react@18.3.18)(react@18.3.1):
|
||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -8306,7 +8438,7 @@ packages:
|
||||
debug: 4.4.0
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.5(typescript@5.6.3)
|
||||
vite: 6.2.0(@types/node@22.13.9)
|
||||
vite: 6.2.0(@types/node@22.13.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
Reference in New Issue
Block a user