Merge remote-tracking branch 'origin/main' into iipbs/common/main

This commit is contained in:
YangJinSoo94
2025-03-26 14:41:05 +09:00
22 changed files with 449 additions and 138 deletions
+3 -1
View File
@@ -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;
+5
View File
@@ -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;
+1 -1
View File
@@ -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"
+81 -19
View File
@@ -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;
}
}
+1 -4
View File
@@ -11,10 +11,7 @@ import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
};
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelect({ upCd, defaultValue, ...props }: CodeSelectProps) {
const { data: code } = useQuery({
@@ -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<typeof siteFormSchema>;
+9 -2
View File
@@ -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<Menu[]> {
public async getTreeMenuList(siteId: string): Promise<Menu[]> {
const response = await client.get<Menu[]>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
);
return response.data;
}
public async modifyMenu(siteId: string, data: TreeMenu[]): Promise<void> {
public async modifyTreeMenu(siteId: string, data: TreeMenu[]): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
data,
);
return response.data;
}
public async modifyMenuName(menuCd: number, name: string): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
);
return response.data;
}
}
export const MenuApi = new MenuApiService();
+38 -1
View File
@@ -26,8 +26,14 @@ export const generateNewTreeData = (
options: DropOptions<Menu>,
): 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<Menu>) => void) => (node: NodeModel<Menu>) => {
callback(node);
};
export const handleDropNode =
(callback: (newTree: TreeMenu) => void) =>
(newTree: NodeModel<Menu>[], options: DropOptions<Menu>) => {
const newTreeData = generateNewTreeData(newTree, options);
if (!newTreeData) return;
callback(newTreeData);
};
export const handleNodeTextChange =
(treeData: NodeModel<Menu>[]) =>
(callback: (newTree: NodeModel<Menu>[]) => void) =>
(id: NodeModel["id"], value: string) => {
const newTree = treeData.map((node) => {
if (node.id === id) {
return {
...node,
text: value,
};
}
return node;
});
callback(newTree);
};
@@ -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<typeof menuFormSchema>;
export interface MenuService {
getMenuList: (siteId: string) => Promise<Menu[]>;
modifyMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
getTreeMenuList: (siteId: string) => Promise<Menu[]>;
modifyTreeMenu: (siteId: string, data: TreeMenu[]) => Promise<void>;
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
}
@@ -70,7 +70,11 @@ export function BaseBottomMenuForm({
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="otptY" />
@@ -98,7 +102,11 @@ export function BaseBottomMenuForm({
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup
{...field}
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
+116
View File
@@ -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<Menu>;
depth: number;
isOpen: boolean;
isSelected: boolean;
onToggle: (id: NodeModel["id"]) => void;
onSelect: (node: NodeModel<Menu>) => 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 (
<div
style={{ paddingInlineStart: indent }}
className={cn(
"group flex cursor-pointer items-center justify-between gap-2 rounded-sm p-2 hover:bg-accent",
isSelected && "!bg-[rgba(32,148,250,0.2)]",
)}
onClick={handleSelect}
>
<div className="flex items-center">
{editMode ? (
<>
<Input
value={nodeName}
onChange={(e) => setNodeName(e.target.value)}
className="h-[2.5rem] !text-[1.3rem] text-foreground"
/>
</>
) : (
<>
{data?.children?.length && (
<i
className={cn(
"diveicon di-arrow-forward mr-2 text-base text-label",
isOpen && "rotate-90",
)}
onClick={handleToggle}
/>
)}
<span className="text-[1.4rem] font-medium text-label">{text}</span>
</>
)}
</div>
{editMode ? (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={handleSave}
>
<i className="diveicon di-check text-2xl" />
</Button>
<Button
variant="ghost"
size="icon"
className="size-8 !bg-transparent"
onClick={() => {
setEditMode(false);
setNodeName(text);
}}
>
<i className="diveicon di-x" />
</Button>
</div>
) : (
<Button
variant="ghost"
size="icon"
className="invisible !bg-transparent group-hover:visible"
onClick={() => setEditMode(true)}
>
<i className="diveicon di-pencil" />
</Button>
)}
</div>
);
}
export { MenuNode };
+3 -3
View File
@@ -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 (
<PageLayout pageTitle="메뉴 관리">
<div className="card flex gap-2">
<div className="card card-border">
<div className="card card-border w-[24rem]">
<Select value={siteId} onValueChange={setSiteId}>
<SelectTrigger className="w-[18rem]">
<SelectTrigger>
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
+25 -74
View File
@@ -8,13 +8,12 @@ import {
MultiBackend,
getBackendOptions,
type NodeModel,
type DropOptions,
} from "@minoru/react-dnd-treeview";
import { cn } from "@common/components/lib";
import { ENDPOINT } from "@/shared/config";
import { MenuApi } from "../api/menu-service";
import type { Menu, TreeMenu } from "../model/menu-interface";
import { generateNewTreeData } from "../lib/utils";
import { ENDPOINT } from "@/shared/config";
import { handleDropNode, handleSelectNode } from "../lib/utils";
import { MenuNode } from "./menu-node";
type MenuTreeProps = {
treeData: NodeModel<Menu>[];
@@ -24,9 +23,9 @@ type MenuTreeProps = {
function MenuTree({ treeData, siteId }: MenuTreeProps) {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: ({ data }: { data: TreeMenu[] }) =>
MenuApi.modifyMenu(siteId, data),
const { mutate: modifyTreeMenu } = useMutation({
mutationFn: (newTree: TreeMenu) =>
MenuApi.modifyTreeMenu(siteId, [newTree]),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
@@ -38,17 +37,6 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
null,
);
const handleSelect = (node: NodeModel<Menu>) => setSelectedNode(node);
const handleDrop = (
newTree: NodeModel<Menu>[],
options: DropOptions<Menu>,
) => {
const newTreeData = generateNewTreeData(newTree, options);
if (!newTreeData) return;
mutate({ data: [newTreeData] });
};
return (
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
@@ -61,72 +49,35 @@ function MenuTree({ treeData, siteId }: MenuTreeProps) {
isOpen={isOpen}
isSelected={selectedNode?.id === node.id}
onToggle={onToggle}
onSelect={handleSelect}
onSelect={handleSelectNode(setSelectedNode)}
/>
)}
onDrop={handleDrop}
onDrop={handleDropNode(modifyTreeMenu)}
classes={{
root: "p-4",
container: "flex flex-col",
listItem: "",
dropTarget: "!bg-[rgba(32,148,250,0.5)] rounded-sm",
draggingSource: "",
placeholder: "",
placeholder: "relative",
}}
sort={false}
dropTargetOffset={5}
insertDroppableFirst={false}
canDrop={(_, { dragSource, dropTargetId }) => {
if (dragSource?.parent === dropTargetId) {
return true;
}
}}
placeholderRender={(_, { depth }) => {
return (
<div
className="absolute right-0 top-0 h-[0.2rem] bg-primary"
style={{ left: depth * 10 + 5 }}
/>
);
}}
/>
</DndProvider>
);
}
type MenuNodeProps = {
node: NodeModel<Menu>;
depth: number;
isOpen: boolean;
isSelected: boolean;
onToggle: (id: NodeModel["id"]) => void;
onSelect: (node: NodeModel<Menu>) => void;
};
function MenuNode({
node,
depth,
isOpen,
isSelected,
onToggle,
onSelect,
}: MenuNodeProps) {
const { id, text, data } = node;
const indent = depth * 10 + 5;
const handleToggle = () => onToggle(id);
const handleSelect = () => onSelect(node);
return (
<div
style={{ paddingInlineStart: indent }}
className={cn(
"flex cursor-pointer items-center gap-2 rounded-sm p-2 hover:bg-accent",
isSelected && "!bg-[rgba(32,148,250,0.2)]",
)}
onClick={handleSelect}
>
<div className="flex items-center">
{data?.children?.length && (
<i
className={cn(
"diveicon di-arrow-forward mr-2 text-label",
isOpen && "rotate-90",
)}
onClick={(e) => {
e.stopPropagation();
handleToggle();
}}
/>
)}
<span className="text-[1.4rem] font-medium text-label">{text}</span>
</div>
</div>
);
}
export { MenuTree };
@@ -17,8 +17,8 @@ const columns = [
id: "siteNm",
header: "사이트명",
}),
columnHelper.accessor("siteAddr", {
id: "siteAddr",
columnHelper.accessor("dmnAddr", {
id: "dmnAddr",
header: "대표 URL",
}),
columnHelper.accessor("bscSiteYn", {
@@ -16,6 +16,7 @@ function ModifyPage({ siteId }: { siteId: string }) {
const router = useRouter();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const { data: siteDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.SITES, siteId],
queryFn: () => SiteApi.siteDetail(siteId),
+7 -3
View File
@@ -43,10 +43,12 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
const [onConfirm, setOnConfirm] = useState(false);
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
const [domainUrl, setDomainUrl] = useState("");
const handleSaveDomain = (rows: Domain[]) => {
setDomainUrl(rows.find((row) => row.rprsDmnYn === "Y")?.dmnAddr || "");
form.setValue(
"dmnAddr",
rows.find((row) => row.rprsDmnYn === "Y")?.dmnAddr || "",
);
};
const onSubmit = async (data: SiteFormData) => {
@@ -99,6 +101,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="w-[12rem] !text-[1.3rem]"
disabled={isModifyMode}
/>
@@ -173,6 +176,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="사이트명을 입력하십시오."
/>
@@ -211,7 +215,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
<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">
{domainUrl}
{form.watch("dmnAddr")}
</div>
<DomainManagementDialog
siteId={siteId}
-1
View File
@@ -41,7 +41,6 @@ function SitePage() {
useEffect(() => {
refetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
@@ -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;
}
}
@@ -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;
+1
View File
@@ -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,
@@ -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<HTMLInputElement>) => {
const value = e.target.value;
setInputValue(value);
setFilteredIconNames(iconNames.filter((icon) => icon.includes(value)));
};
return (
@@ -24,18 +40,34 @@ const IconFinder = ({ handleClick }: IconFinderProps) => {
아이콘 찾기
</Button>
</DialogTrigger>
<DialogContent className="max-h-[65rem] max-w-[65rem] overflow-y-auto p-12">
<div className="py-4">
<div className="grid grid-cols-5 gap-6 text-[2.4rem]">
{iconNames.map((icon) => (
<IconItem
key={icon}
icon={icon}
selectedIcon={selectedIcon}
handleClick={handleIconClick}
/>
))}
</div>
<DialogContent className="flex h-[65rem] max-w-[65rem] flex-col items-start overflow-y-auto">
<div className="sticky top-0 z-10 w-full bg-background">
<DialogHeader className="w-full">
<DialogTitle className="-mt-6 flex items-center justify-between bg-background pt-6">
아이콘 찾기
<DialogClose asChild>
<button className="flex size-6 items-center justify-center rounded-full">
<i className="diveicon di-x text-base text-muted-foreground" />
</button>
</DialogClose>
</DialogTitle>
</DialogHeader>
<Input
value={inputValue}
onChange={handleInputChange}
className="mt-6 !text-[1.3rem] text-foreground"
placeholder="아이콘 이름 검색"
/>
</div>
<div className="grid grid-cols-5 gap-6 text-[2.4rem]">
{filteredIconNames.map((icon) => (
<IconItem
key={icon}
icon={icon}
selectedIcon={selectedIcon}
handleClick={handleIconClick}
/>
))}
</div>
</DialogContent>
</Dialog>
@@ -65,8 +97,8 @@ const IconItem = ({
selectedIcon !== icon && "hidden",
)}
/>
<i className={`diveicon di-${icon}`} />
<span className="text-center text-[1.2rem]">{icon}</span>
<i className={`diveicon di-${icon} text-foreground`} />
<span className="text-center text-[1.2rem] text-foreground">{icon}</span>
</div>
);
};
@@ -3,15 +3,13 @@ import * as React from "react";
import { cn } from "../../lib/utils";
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, value, ...props }, ref) => {
({ className, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-[3.2rem] w-full rounded-md border border-input bg-form px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
)}
value={value ?? ""}
ref={ref}
{...props}
/>