diff --git a/README.md b/README.md index 798425682..e74e7e815 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,9 @@ export default nextConfig; ### 6. globals.css(또는 전역 css) 수정 ```css -@import "@common/assets/index.css"; +@import "@common/assets/styles/base.css"; +@import "@common/assets/styles/diveicon.css"; +@import "@common/assets/styles/components.css"; @tailwind base; @tailwind components; diff --git a/apps/cs/eslint.config.mjs b/apps/cs/eslint.config.mjs index c85fb67c4..9109635e9 100644 --- a/apps/cs/eslint.config.mjs +++ b/apps/cs/eslint.config.mjs @@ -11,6 +11,11 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + "react-hooks/exhaustive-deps": "off", + }, + }, ]; export default eslintConfig; diff --git a/apps/cs/package.json b/apps/cs/package.json index 6e774acba..4394eaf17 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "dev": "NODE_OPTIONS='--inspect' next dev", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/apps/cs/src/app/styles/components.css b/apps/cs/src/app/styles/components.css index b67efb8f0..c1eb79210 100644 --- a/apps/cs/src/app/styles/components.css +++ b/apps/cs/src/app/styles/components.css @@ -1,25 +1,87 @@ -@tailwind components; +.card { + position: relative; + margin: 0.6rem; + box-sizing: border-box; + min-height: 2rem; + border-radius: 0.5rem; + padding: 0.6rem; + font-size: 1.3rem; + color: #666; +} -@layer components { - .card { - @apply relative m-[0.6rem] box-border min-h-[2rem] rounded-[0.5rem] p-[0.6rem] text-[1.3rem] text-[#666]; - } +.card_width10 {width:calc(10% - 12px);} + +.card_width20 {width:calc(20% - 12px);} + +.card_width25 {width:calc(25% - 12px);} + +.card_width30 {width:calc(30% - 12px);} + +.card_width33 {width:calc(33.333% - 12px);} + +.card_width40 {width:calc(40% - 12px);} + +.card_width45 {width:calc(45% - 12px);} + +.card_width50 {width:calc(50% - 12px);} + +.card_width60 {width:calc(60% - 12px);} + +.card_width66 {width:calc(66.666% - 12px);} + +.card_width70 {width:calc(70% - 12px);} + +.card_width75 {width:calc(75% - 12px);} + +.card_width80 {width:calc(80% - 12px);} + +.card_width90 {width:calc(90% - 12px);} + +.card_width100 {width:calc(100% - 12px);} + +.card_nopadding {padding:0 !important;} + +.card_nomargin {width:100%;margin:0 !important;padding:0 !important;} + +.topline {border-top: 1px solid #ccc;} + +.bottomline {border-bottom: 1px solid #e6eef3;background: #fff;} + +.card-border { + border: 1px solid #dbe2e6; + background-color: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.card-header { + margin-left: -0.6rem; + margin-right: -0.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e1e7eb; + padding: 1rem; +} + +.card-footer { + margin-left: -0.6rem; + margin-right: -0.6rem; + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid #e1e7eb; + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* 블랙 테마 */ +[data-theme="black"] { .card-border { - @apply border border-[#dbe2e6] bg-[rgba(255,255,255,0.6)] shadow-blur; + border-color: #3f4042; + background-color: rgba(255, 255, 255, 0.065); } .card-header { - @apply -mx-[0.6rem] flex items-center justify-between border-b border-b-[#e1e7eb] p-4; - } - .card-footer { - @apply -mx-[0.6rem] mt-2 flex items-center justify-center border-t border-t-[#e1e7eb] py-4; - } - - [data-theme="black"] { - .card-border { - @apply border-[#3f4042] bg-[rgba(255,255,255,0.065)]; - } - .card-header { - @apply border-b-[#3f4042]; - } + border-bottom-color: #3f4042; } } diff --git a/apps/cs/src/entities/code/ui/code-select.tsx b/apps/cs/src/entities/code/ui/code-select.tsx index 00b3157aa..bd175a8a9 100644 --- a/apps/cs/src/entities/code/ui/code-select.tsx +++ b/apps/cs/src/entities/code/ui/code-select.tsx @@ -11,10 +11,7 @@ import { CodeApi } from "../api/code-service"; type CodeSelectProps = { upCd: string; - value?: string; - defaultValue?: string; - onValueChange?: (value: string) => void; -}; +} & React.ComponentPropsWithoutRef; function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) { const { data: code } = useQuery({ diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts index 6edc1e69e..ae14297ae 100644 --- a/apps/cs/src/entities/site/model/site-interface.ts +++ b/apps/cs/src/entities/site/model/site-interface.ts @@ -16,6 +16,7 @@ export interface ISite { useYn: string; regDt: string; rgTrNm: string; + dmnAddr: string; } export interface ISiteDetail extends SiteFormData { @@ -44,6 +45,7 @@ export const siteFormSchema = z.object({ bkmkIcon: z.string().max(100).nullable(), useYn: z.string(), siteSkn: z.string().max(10).nullable(), + dmnAddr: z.string().nullable(), }); export type SiteFormData = z.infer; diff --git a/apps/cs/src/pages/menu/api/menu-service.ts b/apps/cs/src/pages/menu/api/menu-service.ts index bb5dd45f4..4fe6c845a 100644 --- a/apps/cs/src/pages/menu/api/menu-service.ts +++ b/apps/cs/src/pages/menu/api/menu-service.ts @@ -3,20 +3,27 @@ import { ENDPOINT } from "@/shared/config"; import type { Menu, MenuService, TreeMenu } from "../model/menu-interface"; export class MenuApiService implements MenuService { - public async getMenuList(siteId: string): Promise { + public async getTreeMenuList(siteId: string): Promise { const response = await client.get( `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, ); return response.data; } - public async modifyMenu(siteId: string, data: TreeMenu[]): Promise { + public async modifyTreeMenu(siteId: string, data: TreeMenu[]): Promise { const response = await client.put( `${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`, data, ); return response.data; } + + public async modifyMenuName(menuCd: number, name: string): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`, + ); + return response.data; + } } export const MenuApi = new MenuApiService(); diff --git a/apps/cs/src/pages/menu/lib/utils.ts b/apps/cs/src/pages/menu/lib/utils.ts index 304feac48..333ebad94 100644 --- a/apps/cs/src/pages/menu/lib/utils.ts +++ b/apps/cs/src/pages/menu/lib/utils.ts @@ -26,8 +26,14 @@ export const generateNewTreeData = ( options: DropOptions, ): TreeMenu | null => { const { dragSourceId, dragSource, dropTargetId, dropTarget } = options; + if (!dragSource?.data) return null; + const siblings = treeData.filter((node) => node.parent === dropTargetId); + + const newSortSeq = + siblings.findIndex((sibling) => sibling.id === dragSourceId) + 1; + const childNodes = treeData.filter( (node) => node.parent === dragSourceId && node.data, ); @@ -37,7 +43,6 @@ export const generateNewTreeData = ( return { menuCd: node.data?.menuCd, name: node.data?.menuKornNm, - sortSeq: node.data?.sortSeq, icon: node.data?.iconNm, }; }; @@ -45,6 +50,7 @@ export const generateNewTreeData = ( const treeMenu: TreeMenu = { ...baseNode(dragSource), parentId: dropTargetId === 0 ? null : (dropTargetId as number), + sortSeq: newSortSeq, levelNo: (dropTarget?.data?.levelNo ?? 0) + 1, }; @@ -53,9 +59,40 @@ export const generateNewTreeData = ( children: childNodes.map((child) => ({ ...baseNode(child), parentId: child.data?.parentId === 0 ? null : (dropTargetId as number), + sortSeq: child.data?.sortSeq, levelNo: (treeMenu?.levelNo ?? 0) + 1, })), }; return result; }; + +export const handleSelectNode = + (callback: (node: NodeModel) => void) => (node: NodeModel) => { + callback(node); + }; + +export const handleDropNode = + (callback: (newTree: TreeMenu) => void) => + (newTree: NodeModel[], options: DropOptions) => { + const newTreeData = generateNewTreeData(newTree, options); + if (!newTreeData) return; + callback(newTreeData); + }; + +export const handleNodeTextChange = + (treeData: NodeModel[]) => + (callback: (newTree: NodeModel[]) => void) => + (id: NodeModel["id"], value: string) => { + const newTree = treeData.map((node) => { + if (node.id === id) { + return { + ...node, + text: value, + }; + } + + return node; + }); + callback(newTree); + }; diff --git a/apps/cs/src/pages/menu/model/menu-interface.ts b/apps/cs/src/pages/menu/model/menu-interface.ts index 09e473d07..fc22c462c 100644 --- a/apps/cs/src/pages/menu/model/menu-interface.ts +++ b/apps/cs/src/pages/menu/model/menu-interface.ts @@ -53,17 +53,18 @@ export const menuFormSchema = z.object({ MENU_TYPE.BOTTOM, ]), menuLnkgSn: z.number().nullish(), - otptYn: z.enum(["Y", "N"]).default("Y"), + otptYn: z.enum(["Y", "N"]).default("Y").nullish(), prntMenuCd: z.number().nullish(), sortSeq: z.number(), evlUseYn: z.enum(["Y", "N"]).nullish(), qrcd: z.string().nullish(), - useYn: z.enum(["Y", "N"]).default("Y"), + useYn: z.enum(["Y", "N"]).default("Y").nullish(), }); export type MenuFormData = z.infer; export interface MenuService { - getMenuList: (siteId: string) => Promise; - modifyMenu: (siteId: string, data: TreeMenu[]) => Promise; + getTreeMenuList: (siteId: string) => Promise; + modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise; + modifyMenuName: (menuCd: number, name: string) => Promise; } diff --git a/apps/cs/src/pages/menu/ui/base-bottom-menu-form.tsx b/apps/cs/src/pages/menu/ui/base-bottom-menu-form.tsx index 84527d2da..d8afc344d 100644 --- a/apps/cs/src/pages/menu/ui/base-bottom-menu-form.tsx +++ b/apps/cs/src/pages/menu/ui/base-bottom-menu-form.tsx @@ -70,7 +70,11 @@ export function BaseBottomMenuForm({ render={({ field }) => ( - +
@@ -98,7 +102,11 @@ export function BaseBottomMenuForm({ render={({ field }) => ( - +
diff --git a/apps/cs/src/pages/menu/ui/menu-node.tsx b/apps/cs/src/pages/menu/ui/menu-node.tsx new file mode 100644 index 000000000..f8a150633 --- /dev/null +++ b/apps/cs/src/pages/menu/ui/menu-node.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import type { NodeModel } from "@minoru/react-dnd-treeview"; +import { cn } from "@common/components/lib"; +import { Button, Input } from "@common/components/ui"; +import type { Menu } from "../model/menu-interface"; +import { MenuApi } from "../api/menu-service"; + +type MenuNodeProps = { + node: NodeModel; + depth: number; + isOpen: boolean; + isSelected: boolean; + onToggle: (id: NodeModel["id"]) => void; + onSelect: (node: NodeModel) => void; +}; + +function MenuNode({ + node, + depth, + isOpen, + isSelected, + onToggle, + onSelect, +}: MenuNodeProps) { + const { id, text, data } = node; + const indent = depth * 10 + 5; + + const { mutateAsync: modifyMenuName } = useMutation({ + mutationFn: (nodeName: string) => { + const encodedName = encodeURIComponent(nodeName); + return MenuApi.modifyMenuName(id as number, encodedName); + }, + }); + + const [editMode, setEditMode] = useState(false); + const [nodeName, setNodeName] = useState(text); + + const handleToggle = () => onToggle(id); + const handleSelect = () => onSelect(node); + + const handleSave = async () => { + await modifyMenuName(nodeName); + setEditMode(false); + }; + + return ( +
+
+ {editMode ? ( + <> + setNodeName(e.target.value)} + className="h-[2.5rem] !text-[1.3rem] text-foreground" + /> + + ) : ( + <> + {data?.children?.length && ( + + )} + {text} + + )} +
+ {editMode ? ( +
+ + +
+ ) : ( + + )} +
+ ); +} + +export { MenuNode }; diff --git a/apps/cs/src/pages/menu/ui/menu-page.tsx b/apps/cs/src/pages/menu/ui/menu-page.tsx index 760f86c42..5cc63b627 100644 --- a/apps/cs/src/pages/menu/ui/menu-page.tsx +++ b/apps/cs/src/pages/menu/ui/menu-page.tsx @@ -30,7 +30,7 @@ function MenuPage() { const { data: treeData } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId], - queryFn: () => MenuApi.getMenuList(siteId), + queryFn: () => MenuApi.getTreeMenuList(siteId), select: (data) => generateTreeData(data), enabled: !!siteId, }); @@ -61,9 +61,9 @@ function MenuPage() { return (
-
+
@@ -173,6 +176,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) { @@ -211,7 +215,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
- {domainUrl} + {form.watch("dmnAddr")}
{ refetch(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/packages/common/assets/src/styles/components.css b/packages/common/assets/src/styles/components.css new file mode 100644 index 000000000..c1eb79210 --- /dev/null +++ b/packages/common/assets/src/styles/components.css @@ -0,0 +1,87 @@ +.card { + position: relative; + margin: 0.6rem; + box-sizing: border-box; + min-height: 2rem; + border-radius: 0.5rem; + padding: 0.6rem; + font-size: 1.3rem; + color: #666; +} + +.card_width10 {width:calc(10% - 12px);} + +.card_width20 {width:calc(20% - 12px);} + +.card_width25 {width:calc(25% - 12px);} + +.card_width30 {width:calc(30% - 12px);} + +.card_width33 {width:calc(33.333% - 12px);} + +.card_width40 {width:calc(40% - 12px);} + +.card_width45 {width:calc(45% - 12px);} + +.card_width50 {width:calc(50% - 12px);} + +.card_width60 {width:calc(60% - 12px);} + +.card_width66 {width:calc(66.666% - 12px);} + +.card_width70 {width:calc(70% - 12px);} + +.card_width75 {width:calc(75% - 12px);} + +.card_width80 {width:calc(80% - 12px);} + +.card_width90 {width:calc(90% - 12px);} + +.card_width100 {width:calc(100% - 12px);} + +.card_nopadding {padding:0 !important;} + +.card_nomargin {width:100%;margin:0 !important;padding:0 !important;} + +.topline {border-top: 1px solid #ccc;} + +.bottomline {border-bottom: 1px solid #e6eef3;background: #fff;} + +.card-border { + border: 1px solid #dbe2e6; + background-color: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.card-header { + margin-left: -0.6rem; + margin-right: -0.6rem; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #e1e7eb; + padding: 1rem; +} + +.card-footer { + margin-left: -0.6rem; + margin-right: -0.6rem; + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + border-top: 1px solid #e1e7eb; + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* 블랙 테마 */ +[data-theme="black"] { + .card-border { + border-color: #3f4042; + background-color: rgba(255, 255, 255, 0.065); + } + .card-header { + border-bottom-color: #3f4042; + } +} diff --git a/packages/common/assets/src/styles/index.ts b/packages/common/assets/src/styles/index.ts index 807e81006..8b6116d95 100644 --- a/packages/common/assets/src/styles/index.ts +++ b/packages/common/assets/src/styles/index.ts @@ -2,6 +2,7 @@ export const styles = { base: require("./base.css"), diveicon: require("./diveicon.css"), grid: require("./grid.css"), + components: require("./components.css"), } as const; export type StyleType = keyof typeof styles; diff --git a/packages/common/assets/tsup.config.ts b/packages/common/assets/tsup.config.ts index 450a1b173..59c4afaeb 100644 --- a/packages/common/assets/tsup.config.ts +++ b/packages/common/assets/tsup.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ "src/styles/base.css", "src/styles/diveicon.css", "src/styles/grid.css", + "src/styles/components.css", ], format: ["cjs", "esm"], dts: true, diff --git a/packages/common/components/src/ui/icon-finder/index.tsx b/packages/common/components/src/ui/icon-finder/index.tsx index 62e6e195a..326eb7be8 100644 --- a/packages/common/components/src/ui/icon-finder/index.tsx +++ b/packages/common/components/src/ui/icon-finder/index.tsx @@ -1,19 +1,35 @@ import { useState } from "react"; -import { Button } from "../button"; -import { Dialog, DialogContent, DialogTrigger } from "../dialog"; import { cn } from "../../lib"; +import { Button } from "../button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../dialog"; +import { Input } from "../input"; import iconNames from "./data"; type IconFinderProps = { - handleClick: (icon: string) => void; + handleClick?: (icon: string) => void; }; const IconFinder = ({ handleClick }: IconFinderProps) => { const [selectedIcon, setSelectedIcon] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [filteredIconNames, setFilteredIconNames] = useState(iconNames); const handleIconClick = (icon: string) => { setSelectedIcon(icon); - handleClick(icon); + handleClick?.(icon); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + setFilteredIconNames(iconNames.filter((icon) => icon.includes(value))); }; return ( @@ -24,18 +40,34 @@ const IconFinder = ({ handleClick }: IconFinderProps) => { 아이콘 찾기 - -
-
- {iconNames.map((icon) => ( - - ))} -
+ +
+ + + 아이콘 찾기 + + + + + + +
+
+ {filteredIconNames.map((icon) => ( + + ))}
@@ -65,8 +97,8 @@ const IconItem = ({ selectedIcon !== icon && "hidden", )} /> - - {icon} + + {icon}
); }; diff --git a/packages/common/components/src/ui/input/index.tsx b/packages/common/components/src/ui/input/index.tsx index a980f0ebd..11cbca71d 100644 --- a/packages/common/components/src/ui/input/index.tsx +++ b/packages/common/components/src/ui/input/index.tsx @@ -3,15 +3,13 @@ import * as React from "react"; import { cn } from "../../lib/utils"; const Input = React.forwardRef>( - ({ className, type, value, ...props }, ref) => { + ({ className, ...props }, ref) => { return (