wing-ops/frontend/src/tabs/admin/components/SortableMenuItem.tsx

173 lines
6.5 KiB
TypeScript
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import data from '@emoji-mart/data';
import EmojiPicker from '@emoji-mart/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { type MenuConfigItem } from '@common/services/authApi';
// ─── 메뉴 항목 (Sortable) ────────────────────────────────────
export interface SortableMenuItemProps {
menu: MenuConfigItem;
idx: number;
totalCount: number;
isEditing: boolean;
emojiPickerId: string | null;
emojiPickerRef: React.RefObject<HTMLDivElement | null>;
onToggle: (id: string) => void;
onMove: (idx: number, direction: -1 | 1) => void;
onEditStart: (id: string) => void;
onEditEnd: () => void;
onEmojiPickerToggle: (id: string | null) => void;
onLabelChange: (id: string, value: string) => void;
onEmojiSelect: (emoji: { native: string }) => void;
}
function SortableMenuItem({
menu,
idx,
totalCount,
isEditing,
emojiPickerId,
emojiPickerRef,
onToggle,
onMove,
onEditStart,
onEditEnd,
onEmojiPickerToggle,
onLabelChange,
onEmojiSelect,
}: SortableMenuItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: menu.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
zIndex: isDragging ? 50 : undefined,
};
return (
<div
ref={setNodeRef}
style={style}
className={`flex items-center justify-between px-4 py-3 rounded-md border transition-all ${
menu.enabled ? 'bg-bg-surface border-stroke' : 'bg-bg-base border-stroke opacity-50'
}`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<button
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing w-6 h-7 flex items-center justify-center text-fg-disabled hover:text-fg transition-all shrink-0"
title="드래그하여 순서 변경"
>
<svg width="12" height="16" viewBox="0 0 12 16" fill="currentColor">
<circle cx="3" cy="2" r="1.5" />
<circle cx="9" cy="2" r="1.5" />
<circle cx="3" cy="8" r="1.5" />
<circle cx="9" cy="8" r="1.5" />
<circle cx="3" cy="14" r="1.5" />
<circle cx="9" cy="14" r="1.5" />
</svg>
</button>
<span className="text-fg-disabled text-caption font-mono w-6 text-center shrink-0">
{idx + 1}
</span>
{isEditing ? (
<>
<div className="relative shrink-0">
<button
onClick={() => onEmojiPickerToggle(emojiPickerId === menu.id ? null : menu.id)}
className="w-10 h-10 text-[20px] bg-bg-elevated border border-stroke rounded flex items-center justify-center hover:border-color-accent transition-all"
title="아이콘 변경"
>
{menu.icon}
</button>
{emojiPickerId === menu.id && (
<div ref={emojiPickerRef} className="absolute top-12 left-0 z-[300]">
<EmojiPicker
data={data}
onEmojiSelect={onEmojiSelect}
theme="dark"
locale="kr"
previewPosition="none"
skinTonePosition="search"
perLine={8}
/>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<input
type="text"
value={menu.label}
onChange={(e) => onLabelChange(menu.id, e.target.value)}
className="w-full h-8 text-title-4 font-semibold font-korean bg-bg-elevated border border-stroke rounded px-2 text-fg focus:border-color-accent focus:outline-none"
/>
<div className="text-caption text-fg-disabled font-mono mt-0.5">{menu.id}</div>
</div>
<button
onClick={onEditEnd}
className="shrink-0 px-2 py-1 text-caption font-semibold text-color-accent border border-color-accent rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
</>
) : (
<>
<span className="text-title-2 shrink-0">{menu.icon}</span>
<div className="flex-1 min-w-0">
<div
className={`text-title-4 font-semibold font-korean ${menu.enabled ? 'text-fg' : 'text-fg-disabled'}`}
>
{menu.label}
</div>
<div className="text-caption text-fg-disabled font-mono">{menu.id}</div>
</div>
<button
onClick={() => onEditStart(menu.id)}
className="shrink-0 w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-label-2 flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all"
title="라벨/아이콘 편집"
>
</button>
</>
)}
</div>
<div className="flex items-center gap-3 ml-3 shrink-0">
<button
onClick={() => onToggle(menu.id)}
className={`relative w-10 h-5 rounded-full transition-all ${
menu.enabled ? 'bg-color-accent' : 'bg-bg-card border border-stroke'
}`}
>
<span
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all ${
menu.enabled ? 'left-[22px]' : 'left-0.5'
}`}
/>
</button>
<div className="flex gap-1">
<button
onClick={() => onMove(idx, -1)}
disabled={idx === 0}
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-caption flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>
<button
onClick={() => onMove(idx, 1)}
disabled={idx === totalCount - 1}
className="w-7 h-7 rounded border border-stroke bg-bg-elevated text-fg-disabled text-caption flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
);
}
export default SortableMenuItem;