diff --git a/apps/cs/app/(with-base-layout)/dashboard/page.tsx b/apps/cs/app/(with-base-layout)/dashboard/page.tsx new file mode 100644 index 000000000..53d564ac6 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/dashboard/page.tsx @@ -0,0 +1,2 @@ +import { DashboardPage } from "@/pages/dashboard"; +export default DashboardPage; diff --git a/apps/cs/app/(with-base-layout)/image/page.tsx b/apps/cs/app/(with-base-layout)/image/page.tsx new file mode 100644 index 000000000..89ca9eeb1 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/image/page.tsx @@ -0,0 +1,2 @@ +import { ImagePage } from "@/pages/image"; +export default ImagePage; diff --git a/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx b/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx new file mode 100644 index 000000000..8df89b17a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx @@ -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 ; +} diff --git a/apps/cs/app/(with-base-layout)/image/register/page.tsx b/apps/cs/app/(with-base-layout)/image/register/page.tsx new file mode 100644 index 000000000..3c29f2bfe --- /dev/null +++ b/apps/cs/app/(with-base-layout)/image/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/image"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/layout.tsx b/apps/cs/app/(with-base-layout)/layout.tsx index dfb290abd..c3254a935 100644 --- a/apps/cs/app/(with-base-layout)/layout.tsx +++ b/apps/cs/app/(with-base-layout)/layout.tsx @@ -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 ( - - {children} + + {children} ); } diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index c9284d69b..81039090b 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -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*"], }; diff --git a/apps/cs/next.config.ts b/apps/cs/next.config.ts index 7c1a12675..cb651cdc0 100644 --- a/apps/cs/next.config.ts +++ b/apps/cs/next.config.ts @@ -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; diff --git a/apps/cs/package.json b/apps/cs/package.json index 4394eaf17..38d378299 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -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" }, diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index b14c3dc96..fb4b65f27 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -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 ( - - - - {children} - - - - + + + + + {children} + + + + + ); }; diff --git a/apps/cs/src/app/styles/components.css b/apps/cs/src/app/styles/components.css index 17cd78834..5b9742c0f 100644 --- a/apps/cs/src/app/styles/components.css +++ b/apps/cs/src/app/styles/components.css @@ -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; diff --git a/apps/cs/src/entities/auth/index.ts b/apps/cs/src/entities/auth/index.ts index dc3fb96c9..97e142225 100644 --- a/apps/cs/src/entities/auth/index.ts +++ b/apps/cs/src/entities/auth/index.ts @@ -1,2 +1,2 @@ export { LoginApi } from "./api/login-service"; -export { AuthWrapper } from "./ui/auth-wrapper"; +export { AuthWrapper } from "./lib/auth-wrapper"; diff --git a/apps/cs/src/entities/auth/ui/auth-wrapper.tsx b/apps/cs/src/entities/auth/lib/auth-wrapper.tsx similarity index 100% rename from apps/cs/src/entities/auth/ui/auth-wrapper.tsx rename to apps/cs/src/entities/auth/lib/auth-wrapper.tsx diff --git a/apps/cs/src/entities/menu/api/menu-service.ts b/apps/cs/src/entities/menu/api/menu-service.ts new file mode 100644 index 000000000..b6c1510cd --- /dev/null +++ b/apps/cs/src/entities/menu/api/menu-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_ROLES}/${siteId}`, + ); + return response.data; + } +} + +export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/entities/menu/index.ts b/apps/cs/src/entities/menu/index.ts new file mode 100644 index 000000000..1fa9dd218 --- /dev/null +++ b/apps/cs/src/entities/menu/index.ts @@ -0,0 +1 @@ +export { MenuWrapper } from "./lib/menu-wrapper"; diff --git a/apps/cs/src/entities/menu/lib/menu-wrapper.tsx b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx new file mode 100644 index 000000000..d473548af --- /dev/null +++ b/apps/cs/src/entities/menu/lib/menu-wrapper.tsx @@ -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 }; diff --git a/apps/cs/src/entities/menu/lib/utils.ts b/apps/cs/src/entities/menu/lib/utils.ts new file mode 100644 index 000000000..5606d7aa2 --- /dev/null +++ b/apps/cs/src/entities/menu/lib/utils.ts @@ -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; +}; diff --git a/apps/cs/src/entities/menu/model/menu-interface.ts b/apps/cs/src/entities/menu/model/menu-interface.ts new file mode 100644 index 000000000..12c7d007a --- /dev/null +++ b/apps/cs/src/entities/menu/model/menu-interface.ts @@ -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; +} diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index 65f6a95a0..37fd7e49a 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -34,7 +34,7 @@ export class SiteApiService implements SiteService { return response.data; } - public async modifySite(siteId: string, data: SiteFormData): Promise { + public async updateSite(siteId: string, data: SiteFormData): Promise { const validatedData = siteFormSchema.parse(data); const response = await client.put( `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts index ef83a53c1..85b1f802f 100644 --- a/apps/cs/src/entities/site/model/site-interface.ts +++ b/apps/cs/src/entities/site/model/site-interface.ts @@ -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; export interface SiteService { getSiteList(params: SiteParams): Promise; siteDetail(siteId: string): Promise; - modifySite(siteId: string, data: SiteFormData): Promise; + updateSite(siteId: string, data: SiteFormData): Promise; createSite(data: SiteFormData): Promise; } diff --git a/apps/cs/src/pages/auth/login/ui/login-page.tsx b/apps/cs/src/pages/auth/login/ui/login-page.tsx index b5545f256..4700a691b 100644 --- a/apps/cs/src/pages/auth/login/ui/login-page.tsx +++ b/apps/cs/src/pages/auth/login/ui/login-page.tsx @@ -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) => { 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 ( diff --git a/apps/cs/src/pages/dashboard/index.ts b/apps/cs/src/pages/dashboard/index.ts new file mode 100644 index 000000000..5b787c2e2 --- /dev/null +++ b/apps/cs/src/pages/dashboard/index.ts @@ -0,0 +1 @@ +export { DashboardPage } from "./ui/dashboard-page"; diff --git a/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx new file mode 100644 index 000000000..4dc693361 --- /dev/null +++ b/apps/cs/src/pages/dashboard/ui/dashboard-page.tsx @@ -0,0 +1,5 @@ +function DashboardPage() { + return <>; +} + +export { DashboardPage }; diff --git a/apps/cs/src/pages/image/api/image-service.ts b/apps/cs/src/pages/image/api/image-service.ts new file mode 100644 index 000000000..af5d95bbe --- /dev/null +++ b/apps/cs/src/pages/image/api/image-service.ts @@ -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 { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.IMAGES}`, + { + params, + }, + ); + return response.data; + } + + public async getImageInfo(imgSn: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`, + ); + return response.data; + } + + public async createImage(data: ImageFormData): Promise { + const response = await client.post(ENDPOINT.CMS_SERVICE.IMAGES, data); + return response.data; + } + + public async updateImage(imgSn: string, data: ImageFormData): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`, + data, + ); + return response.data; + } + + public async deleteImage(imgSn: string): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`); + } +} + +export const ImageApi = new ImageApiService(); diff --git a/apps/cs/src/pages/image/index.ts b/apps/cs/src/pages/image/index.ts new file mode 100644 index 000000000..5c602ca7d --- /dev/null +++ b/apps/cs/src/pages/image/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { ImagePage } from "./ui/image-page"; diff --git a/apps/cs/src/pages/image/lib/utils.ts b/apps/cs/src/pages/image/lib/utils.ts new file mode 100644 index 000000000..c6c8ca101 --- /dev/null +++ b/apps/cs/src/pages/image/lib/utils.ts @@ -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); +}; diff --git a/apps/cs/src/pages/image/model/image-interface.ts b/apps/cs/src/pages/image/model/image-interface.ts new file mode 100644 index 000000000..7748589c7 --- /dev/null +++ b/apps/cs/src/pages/image/model/image-interface.ts @@ -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; +export type ImageList = PageableList; + +export interface ImageService { + getImageList: (params: ImageParams) => Promise; + getImageInfo: (imgSn: string) => Promise; + createImage: (data: ImageFormData) => Promise; + updateImage: (imgSn: string, data: ImageFormData) => Promise; + deleteImage: (imgSn: string) => Promise; +} diff --git a/apps/cs/src/pages/image/model/table-columns.tsx b/apps/cs/src/pages/image/model/table-columns.tsx new file mode 100644 index 000000000..8ae770e0e --- /dev/null +++ b/apps/cs/src/pages/image/model/table-columns.tsx @@ -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[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + 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") : ""; + }, + }, +]; diff --git a/apps/cs/src/pages/image/ui/create-page.tsx b/apps/cs/src/pages/image/ui/create-page.tsx new file mode 100644 index 000000000..5b3ea3924 --- /dev/null +++ b/apps/cs/src/pages/image/ui/create-page.tsx @@ -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({ + resolver: zodResolver(imageFormSchema), + defaultValues: { + atflYn: "Y", + }, + }); + + return ( +
+
+

이미지 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/image/ui/image-form.tsx b/apps/cs/src/pages/image/ui/image-form.tsx new file mode 100644 index 000000000..81ab21726 --- /dev/null +++ b/apps/cs/src/pages/image/ui/image-form.tsx @@ -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; + imgSn?: string; + handleSave: (data: ImageFormData) => Promise; + handleDelete?: () => Promise; +}; + +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(null); + const [progress, setProgress] = useState(0); + + const handleConfirm = async (callback: () => Promise) => { + 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 ( + <> +
+ +
+ + + + + + + + 이미지명 + + ( + + + + + + )} + /> + + + + 정렬순서 + + ( + + + { + const value = e.target.value; + if (isNaN(Number(value))) return; + field.onChange(Number(value)); + }} + className="w-[12rem] !text-[1.3rem]" + placeholder="정렬순서를 입력해주세요" + /> + + + )} + /> + + + + 이미지유형 + + ( + + + + + + )} + /> + + + + 첨부파일 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ {atflYn === "Y" && ( + + 이미지파일 + + ( + + + { + const files = e.target.files; + if (!files) return; + handleUpload(files, { + handleUploadStart: setUpload, + handleProgress, + handleSuccess: setUpload, + handleResponse: handleUploadResponse, + }); + }} + customPreview={ + + } + /> + + + )} + /> + + + )} + {atflYn === "N" && ( + + 외부링크 + + ( + + + + + + )} + /> + + + )} + + 사용여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+
+
+
+
+ + + {isModifyMode && ( + + )} +
+
+ + + + ); +} + +export { ImageForm }; diff --git a/apps/cs/src/pages/image/ui/image-page.tsx b/apps/cs/src/pages/image/ui/image-page.tsx new file mode 100644 index 000000000..48ffcb55c --- /dev/null +++ b/apps/cs/src/pages/image/ui/image-page.tsx @@ -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({}); + const [imageQuery, setImageQuery] = useState({ + 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) => { + e.preventDefault(); + refetch(); + }; + + const handleRowClick = (row: Image) => { + router.push(`/image/register/${row.imgSn}`); + }; + + useEffect(() => { + refetch(); + }, []); + + return ( +
+
+
+ + + setImageQuery({ ...imageQuery, keyword: e.target.value }) + } + /> + +
+
+
+
+

이미지 목록

+ +
+
+ +
+
+
+ ); +} + +export { ImagePage }; diff --git a/apps/cs/src/pages/image/ui/modify-page.tsx b/apps/cs/src/pages/image/ui/modify-page.tsx new file mode 100644 index 000000000..c440c56f9 --- /dev/null +++ b/apps/cs/src/pages/image/ui/modify-page.tsx @@ -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({ + resolver: zodResolver(imageFormSchema), + values: imageDetail, + }); + + return ( +
+
+

이미지 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/menu/api/menu-service.ts b/apps/cs/src/pages/menu/api/menu-service.ts index 4e03f6cdf..7598c2abb 100644 --- a/apps/cs/src/pages/menu/api/menu-service.ts +++ b/apps/cs/src/pages/menu/api/menu-service.ts @@ -24,7 +24,7 @@ export class MenuApiService implements MenuService { return response.data; } - public async modifyMenu( + public async updateMenu( menuCd: number, data: Omit, ): Promise { @@ -49,7 +49,7 @@ export class MenuApiService implements MenuService { return response.data; } - public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise { + public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise { const response = await client.put( `${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 { + public async updateMenuName(menuCd: number, name: string): Promise { const response = await client.put( `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`, ); diff --git a/apps/cs/src/pages/menu/model/table-colums.tsx b/apps/cs/src/pages/menu/model/content-table-colums.tsx similarity index 92% rename from apps/cs/src/pages/menu/model/table-colums.tsx rename to apps/cs/src/pages/menu/model/content-table-colums.tsx index b24b506fd..b8bb28638 100644 --- a/apps/cs/src/pages/menu/model/table-colums.tsx +++ b/apps/cs/src/pages/menu/model/content-table-colums.tsx @@ -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(); diff --git a/apps/cs/src/pages/menu/model/menu-interface.ts b/apps/cs/src/pages/menu/model/menu-interface.ts index 51258fc49..f9650e0e2 100644 --- a/apps/cs/src/pages/menu/model/menu-interface.ts +++ b/apps/cs/src/pages/menu/model/menu-interface.ts @@ -75,12 +75,12 @@ export type Menu = Omit & { export interface MenuService { getMenuInfo: (menuCd: number) => Promise; createMenu: (data: MenuFormData) => Promise; - modifyMenu: ( + updateMenu: ( menuCd: number, data: Omit, ) => Promise; deleteMenu: (menuCd: number) => Promise; getTreeMenuList: (siteId: string) => Promise; - modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise; - modifyMenuName: (menuCd: number, name: string) => Promise; + updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise; + updateMenuName: (menuCd: number, name: string) => Promise; } diff --git a/apps/cs/src/pages/menu/ui/base-menu-form.tsx b/apps/cs/src/pages/menu/ui/base-menu-form.tsx index 62d69bb38..1af5bca3a 100644 --- a/apps/cs/src/pages/menu/ui/base-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/base-menu-form.tsx @@ -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="메뉴명을 입력하십시오." /> - )} /> @@ -79,7 +73,6 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) { placeholder="메뉴 영문명을 입력하십시오." /> - )} /> @@ -118,7 +111,6 @@ function BaseMenuForm({ form, menuCd }: BaseMenuFormProps) { - )} /> diff --git a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx index 60ee794b8..66d939842 100644 --- a/apps/cs/src/pages/menu/ui/content-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/content-search-dialog.tsx @@ -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; diff --git a/apps/cs/src/pages/menu/ui/menu-form.tsx b/apps/cs/src/pages/menu/ui/menu-form.tsx index 48e7589be..a4f21128b 100644 --- a/apps/cs/src/pages/menu/ui/menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/menu-form.tsx @@ -13,7 +13,7 @@ interface MenuFormProps { form: UseFormReturn; 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) => { + const handleConfirm = async (callback: () => Promise) => { 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 }); } }); }; diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx index b2e595923..1cde10b99 100644 --- a/apps/cs/src/pages/menu/ui/menu-node.tsx +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -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); }; diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 52a63eb8a..71ad0042b 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -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; - }) => MenuApi.modifyMenu(menuCd, data), + }) => MenuApi.updateMenu(menuCd, data), onSuccess: () => { alert("수정되었습니다."); queryClient.invalidateQueries({ @@ -197,67 +196,65 @@ function MenuPage() { return ( <> - -
-
- - {siteId && ( - <> -
- - -
-
- -
- - )} -
-
- -
+
+
+ + {siteId && ( + <> +
+ + +
+
+ +
+ + )}
- +
+ +
+
); diff --git a/apps/cs/src/pages/menu/ui/menu-tree.tsx b/apps/cs/src/pages/menu/ui/menu-tree.tsx index 76dc02610..45a4cbb37 100644 --- a/apps/cs/src/pages/menu/ui/menu-tree.tsx +++ b/apps/cs/src/pages/menu/ui/menu-tree.tsx @@ -41,9 +41,9 @@ function MenuTree({ const queryClient = useQueryClient(); const treeMethods = useRef(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", diff --git a/apps/cs/src/pages/menu/ui/type-menu-form.tsx b/apps/cs/src/pages/menu/ui/type-menu-form.tsx index 5081a7e51..b2e6d98e2 100644 --- a/apps/cs/src/pages/menu/ui/type-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/type-menu-form.tsx @@ -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) { } /> - )} /> @@ -153,7 +147,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) {
- )} /> @@ -186,7 +179,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { - )} /> @@ -211,7 +203,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { placeholder="사이트맵에서 다른 이름으로 사용할 경우 입력하십시오." /> - )} /> @@ -255,7 +246,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { - )} /> @@ -287,7 +277,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { - )} /> @@ -322,7 +311,6 @@ function TypeMenuForm({ form, menuType }: TypeMenuFormProps) { - )} /> diff --git a/apps/cs/src/pages/site/ui/create-page.tsx b/apps/cs/src/pages/site/ui/create-page.tsx index 66a9c63c4..e8f8c700d 100644 --- a/apps/cs/src/pages/site/ui/create-page.tsx +++ b/apps/cs/src/pages/site/ui/create-page.tsx @@ -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 ( - +
+
+

사이트 등록

+
- +
); } diff --git a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx index 4a7e854ee..fa754d36b 100644 --- a/apps/cs/src/pages/site/ui/domain-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/domain-management-dialog.tsx @@ -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]" /> - )} /> @@ -253,7 +247,6 @@ function DomainManagementDialog({ }} /> - )} /> @@ -275,20 +268,19 @@ function DomainManagementDialog({ value="Y" id={`useY-${i}`} /> - +
-
- )} /> diff --git a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx index 97721a6d6..dd9ee3748 100644 --- a/apps/cs/src/pages/site/ui/ip-management-dialog.tsx +++ b/apps/cs/src/pages/site/ui/ip-management-dialog.tsx @@ -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]" /> - )} /> @@ -239,20 +233,19 @@ function IPManagementDialog({ value="Y" id={`prmY-${i}`} /> - +
-
- )} /> diff --git a/apps/cs/src/pages/site/ui/modify-page.tsx b/apps/cs/src/pages/site/ui/modify-page.tsx index 1f1061f9d..cf2fa2200 100644 --- a/apps/cs/src/pages/site/ui/modify-page.tsx +++ b/apps/cs/src/pages/site/ui/modify-page.tsx @@ -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 ( - +
+
+

사이트 수정

+
- +
); } diff --git a/apps/cs/src/pages/site/ui/site-form.tsx b/apps/cs/src/pages/site/ui/site-form.tsx index 463b5a6fa..cbf90c95b 100644 --- a/apps/cs/src/pages/site/ui/site-form.tsx +++ b/apps/cs/src/pages/site/ui/site-form.tsx @@ -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) => { + 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 ( <>
-
-
-

- 사이트 등록/수정 -

-
-
- - - - - - - - 사이트 코드 - +
+
+ + + + + + + 사이트 코드 + + ( + + + + + + )} + /> + + + + 사이트 구분 + +
( - - - - - - - )} - /> - - - - 사이트 구분 - -
- ( - - - - - - - )} - /> - {isModifyMode && ( - - )} -
-
-
- - 사이트 디자인 - - ( - )} /> - - + {isModifyMode && ( + + )} +
+
+
+ + 사이트 디자인 + + ( + + + + + + )} + /> + + + + 사이트명 + + ( + + + + + + )} + /> + + + + 사이트 설명 + + ( + + + + + + )} + /> + + + {isModifyMode && ( - 사이트명 + 사이트 도메인 - ( - - - - - - - )} - /> - - - - 사이트 설명 - - ( - - - - - - - )} - /> - - - {isModifyMode && ( - - 사이트 도메인 - -
-
- {form.watch("dmnAddr")} -
- +
+
+ {form.watch("dmnAddr")}
- - - )} - - 사이트 하단 표기 주소 - - ( - - - - - - - )} - /> + +
- - 사이트 하단 표기 전화번호 - - ( - - - - - - - )} - /> - - - - 사이트 하단 표기 팩스번호 - - ( - - - - - - - )} - /> - - - - 사이트 하단 표기 내용 - - ( - - - - - - - )} - /> - - - - 북마크 아이콘 - - ( - - - - - - - )} - /> - - - - 기본 사이트 여부 - - ( - - - -
- - -
-
- - -
-
-
- -
- )} - /> -
-
- - 사용 여부 - - ( - - - -
- - -
-
- - -
-
-
- -
- )} - /> -
-
- -
-
+ )} + + 사이트 하단 표기 주소 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 전화번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 팩스번호 + + ( + + + + + + )} + /> + + + + 사이트 하단 표기 내용 + + ( + + + + + + )} + /> + + + + 북마크 아이콘 + + ( + + + + + + )} + /> + + + + 기본 사이트 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 사용 여부 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ +
- +
+
+
+ + setSiteQuery({ siteNm: e.target.value })} + /> + +
+
+
+
+

사이트 목록

+
-
-
-

사이트 목록

- -
-
- -
+
+
- +
); } diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index ff519b649..4430285d8 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -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", diff --git a/apps/cs/src/shared/lib/index.ts b/apps/cs/src/shared/lib/index.ts index 259525de6..bfbe51702 100644 --- a/apps/cs/src/shared/lib/index.ts +++ b/apps/cs/src/shared/lib/index.ts @@ -1,2 +1,3 @@ export { getCookie, setCookie, removeCookie } from "./cookie"; +export { useMenuStore } from "./use-menu-store"; export { useUserStore } from "./use-user-store"; diff --git a/apps/cs/src/shared/lib/use-menu-store.ts b/apps/cs/src/shared/lib/use-menu-store.ts new file mode 100644 index 000000000..66e41c76c --- /dev/null +++ b/apps/cs/src/shared/lib/use-menu-store.ts @@ -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 = (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); +}; diff --git a/apps/cs/src/shared/providers/store-provider.tsx b/apps/cs/src/shared/providers/store-provider.tsx index 4814705ab..82783bdac 100644 --- a/apps/cs/src/shared/providers/store-provider.tsx +++ b/apps/cs/src/shared/providers/store-provider.tsx @@ -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; + menuStore: ReturnType; userStore: ReturnType; }; @@ -18,6 +18,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => { if (storeRef.current === null) { storeRef.current = { alertStore: createAlertStore(), + menuStore: createMenuStore(), userStore: createUserStore(), }; } diff --git a/apps/cs/src/shared/store/index.ts b/apps/cs/src/shared/store/index.ts index 4dae19ced..73c608f05 100644 --- a/apps/cs/src/shared/store/index.ts +++ b/apps/cs/src/shared/store/index.ts @@ -1 +1,3 @@ +export { createAlertStore } from "./alert-store"; +export { createMenuStore } from "./menu-store"; export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/cs/src/shared/store/menu-store.ts b/apps/cs/src/shared/store/menu-store.ts new file mode 100644 index 000000000..f0c6340c9 --- /dev/null +++ b/apps/cs/src/shared/store/menu-store.ts @@ -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()((set) => ({ + ...initState, + setCurrentMenuInfo: (menu: MenuItem) => set({ currentMenuInfo: menu }), + })); +}; diff --git a/apps/cs/src/shared/ui/page-layout.tsx b/apps/cs/src/shared/ui/page-layout.tsx index e4f11141b..6813173a1 100644 --- a/apps/cs/src/shared/ui/page-layout.tsx +++ b/apps/cs/src/shared/ui/page-layout.tsx @@ -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 (
-

- {pageTitle} -

+ {children}
); diff --git a/packages/common/components/.storybook/main.ts b/packages/common/components/.storybook/main.ts index 87a748dfd..4b70a4aed 100644 --- a/packages/common/components/.storybook/main.ts +++ b/packages/common/components/.storybook/main.ts @@ -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; }, diff --git a/packages/common/components/package.json b/packages/common/components/package.json index ca2986403..d9ba2a55e 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -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", diff --git a/packages/common/components/src/api/menu.ts b/packages/common/components/src/api/menu.ts index e9899c111..4855718a2 100644 --- a/packages/common/components/src/api/menu.ts +++ b/packages/common/components/src/api/menu.ts @@ -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; } diff --git a/packages/common/components/src/api/site.ts b/packages/common/components/src/api/site.ts index 77ed968e4..2ca35bd26 100644 --- a/packages/common/components/src/api/site.ts +++ b/packages/common/components/src/api/site.ts @@ -20,7 +20,7 @@ export interface SiteInfo { mdfcnDt: string; mdFr: string; rgTrNm: string; - mdFrNm: string; + mdfrNm: string; } export const getSiteInfo = async (siteId: string) => { diff --git a/packages/common/components/src/config/env.ts b/packages/common/components/src/config/env.ts index d75d108a3..dfe49efdb 100644 --- a/packages/common/components/src/config/env.ts +++ b/packages/common/components/src/config/env.ts @@ -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"; diff --git a/packages/common/components/src/lib/index.ts b/packages/common/components/src/lib/index.ts index 04f69ba83..e00099877 100644 --- a/packages/common/components/src/lib/index.ts +++ b/packages/common/components/src/lib/index.ts @@ -1,2 +1,3 @@ -export * from "./utils"; export * from "./cookie"; +export * from "./utils"; +export * from "./use-mobile"; diff --git a/packages/common/components/src/provider/query-provider.tsx b/packages/common/components/src/provider/query-provider.tsx index 5781919f1..e06880255 100644 --- a/packages/common/components/src/provider/query-provider.tsx +++ b/packages/common/components/src/provider/query-provider.tsx @@ -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 ( - {children} - ); + if (!queryClient) { + const queryClient = new QueryClient(); + return ( + {children} + ); + } + + return <>{children}; } diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index 8dc30c701..7e3af5602 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -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({ table }: { table: TableType }) { type TableProps = { table: TableType; + variant?: "default" | "secondary" | "line"; onRowClick?: (row: T) => void; }; -function DataTable({ table, onRowClick }: TableProps) { +function DataTable({ + table, + variant = "default", + onRowClick, +}: TableProps) { return ( - +
{table.getRowModel().rows.map((row) => ( 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; diff --git a/packages/common/components/src/ui/file-search/file-search.css b/packages/common/components/src/ui/file-search/file-search.css deleted file mode 100644 index c8a1e3975..000000000 --- a/packages/common/components/src/ui/file-search/file-search.css +++ /dev/null @@ -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; - } -} diff --git a/packages/common/components/src/ui/file-search/file-search.stories.tsx b/packages/common/components/src/ui/file-search/file-search.stories.tsx deleted file mode 100644 index fdef8da99..000000000 --- a/packages/common/components/src/ui/file-search/file-search.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { FileSearch } from "."; - -const meta: Meta = { - title: "UI/FileSearch", - component: FileSearch, - parameters: { - docs: { - description: { - component: ` -- [Docs](https://ui.shadcn.com/docs/components/input#file) -`, - }, - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => , -}; diff --git a/packages/common/components/src/ui/file-search/index.tsx b/packages/common/components/src/ui/file-search/index.tsx deleted file mode 100644 index 9c9043b60..000000000 --- a/packages/common/components/src/ui/file-search/index.tsx +++ /dev/null @@ -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 & { - id: string; - files?: ReadonlyArray; -}; - -export function FileSearch({ - id, - className, - files, - ...props -}: FileSearchProps) { - return ( -
- -
- {files?.filter(validFileType).map(combineFileName).join(", ")} -
- -
- ); -} diff --git a/packages/common/components/src/ui/file-uploader/file-uploader.css b/packages/common/components/src/ui/file-uploader/file-uploader.css new file mode 100644 index 000000000..4dd70bc7f --- /dev/null +++ b/packages/common/components/src/ui/file-uploader/file-uploader.css @@ -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; + } +} diff --git a/packages/common/components/src/ui/file-uploader/file-uploader.stories.tsx b/packages/common/components/src/ui/file-uploader/file-uploader.stories.tsx new file mode 100644 index 000000000..a849f2b14 --- /dev/null +++ b/packages/common/components/src/ui/file-uploader/file-uploader.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { FileUploader } from "."; + +const meta: Meta = { + title: "UI/FileUploader", + component: FileUploader, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => {}} />, +}; diff --git a/packages/common/components/src/ui/file-uploader/index.tsx b/packages/common/components/src/ui/file-uploader/index.tsx new file mode 100644 index 000000000..da3e04b67 --- /dev/null +++ b/packages/common/components/src/ui/file-uploader/index.tsx @@ -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 & { + id: string; + files?: ReadonlyArray; + customPreview?: React.ReactNode; + onUpload: (e: React.ChangeEvent) => void; +}; + +export function FileUploader({ + id, + files, + customPreview, + className, + onUpload, + ...props +}: FileUploaderProps) { + return ( +
+ + {customPreview || ( +
+ {files?.filter(validFileType).map(combineFileName).join(", ")} +
+ )} + +
+ ); +} diff --git a/packages/common/components/src/ui/file-search/utils.ts b/packages/common/components/src/ui/file-uploader/utils.ts similarity index 100% rename from packages/common/components/src/ui/file-search/utils.ts rename to packages/common/components/src/ui/file-uploader/utils.ts diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 650eb30e1..c3aad6220 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -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"; diff --git a/packages/common/components/src/ui/layout/base.tsx b/packages/common/components/src/ui/layout/base.tsx index b48e2d9d9..21d2ca7c6 100644 --- a/packages/common/components/src/ui/layout/base.tsx +++ b/packages/common/components/src/ui/layout/base.tsx @@ -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 ( - +
- +
{children} diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index 197a44a9e..41dc06bfd 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -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([]); 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({ @@ -90,15 +90,15 @@ export function LNB({ -
+
- + {item.menuKornNm} diff --git a/packages/common/components/src/ui/page-title/index.tsx b/packages/common/components/src/ui/page-title/index.tsx new file mode 100644 index 000000000..5190f1a48 --- /dev/null +++ b/packages/common/components/src/ui/page-title/index.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +const PageTitle = ({ + title, + menuPaths, +}: { + title: string; + menuPaths: string[]; +}) => { + return ( +
+

{title}

+ {menuPaths.length > 0 && ( +
+ + {menuPaths?.map((path, i) => ( + + {i !== 0 && ( + + )} + {path} + + ))} +
+ )} +
+ ); +}; + +export { PageTitle }; diff --git a/packages/common/components/src/ui/page-title/page-title.stories.tsx b/packages/common/components/src/ui/page-title/page-title.stories.tsx new file mode 100644 index 000000000..127e2a0fa --- /dev/null +++ b/packages/common/components/src/ui/page-title/page-title.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { PageTitle } from "."; + +const meta: Meta = { + title: "UI/PageTitle", +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+ +
+ ), +}; diff --git a/packages/common/components/src/ui/progress/index.tsx b/packages/common/components/src/ui/progress/index.tsx new file mode 100644 index 000000000..edb9b92af --- /dev/null +++ b/packages/common/components/src/ui/progress/index.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/packages/common/components/src/ui/sidebar/index.tsx b/packages/common/components/src/ui/sidebar/index.tsx index 317fb46de..bce00bb84 100644 --- a/packages/common/components/src/ui/sidebar/index.tsx +++ b/packages/common/components/src/ui/sidebar/index.tsx @@ -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"; diff --git a/packages/common/components/src/ui/table/index.tsx b/packages/common/components/src/ui/table/index.tsx index 46c8db39c..ba50e9d75 100644 --- a/packages/common/components/src/ui/table/index.tsx +++ b/packages/common/components/src/ui/table/index.tsx @@ -9,7 +9,7 @@ interface TableProps extends React.HTMLAttributes { } const Table = React.forwardRef( - ({ className, variant, ...props }, ref) => ( + ({ className, variant = "default", ...props }, ref) => (
= 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