173 lines
6.5 KiB
TypeScript
173 lines
6.5 KiB
TypeScript
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;
|