wing-ops/frontend/src/tabs/admin/components/SortableMenuItem.tsx
htlee c727afd1ba refactor(frontend): 대형 View 서브탭 단위 분할 + FEATURE_ID 체계 도입
6개 대형 View(AerialView, AssetsView, ReportsView, PreScatView, AdminView, LeftPanel)를
서브탭 단위로 분할하여 모듈 경계를 명확히 함.

- AerialView (2,526줄 → 8파일): MediaManagement, OilAreaAnalysis, RealtimeDrone 등
- AssetsView (2,047줄 → 8파일): AssetManagement, AssetMap, ShipInsurance 등
- ReportsView (1,596줄 → 5파일): TemplateFormEditor, ReportGenerator 등
- PreScatView (1,390줄 → 7파일): ScatLeftPanel, ScatMap, ScatPopup 등
- AdminView (1,306줄 → 7파일): UsersPanel, PermissionsPanel, MenusPanel 등
- LeftPanel (1,237줄 → 5파일): PredictionInputSection, InfoLayerSection, OilBoomSection 등

FEATURE_ID 레지스트리(common/constants/featureIds.ts) 및
감사로그 서브탭 추적 훅(useFeatureTracking) 추가.

.gitignore의 scat/ → /scat/ 수정 (scat 탭 파일 추적 누락 수정)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 16:19:22 +09:00

162 lines
6.3 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-1 border-border'
: 'bg-bg-0 border-border 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-text-3 hover:text-text-1 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-text-3 text-xs 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-2 border border-border rounded flex items-center justify-center hover:border-primary-cyan 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-[13px] font-semibold font-korean bg-bg-2 border border-border rounded px-2 text-text-1 focus:border-primary-cyan focus:outline-none"
/>
<div className="text-[10px] text-text-3 font-mono mt-0.5">{menu.id}</div>
</div>
<button
onClick={onEditEnd}
className="shrink-0 px-2 py-1 text-[10px] font-semibold text-primary-cyan border border-primary-cyan rounded hover:bg-[rgba(6,182,212,0.1)] transition-all font-korean"
>
</button>
</>
) : (
<>
<span className="text-[16px] shrink-0">{menu.icon}</span>
<div className="flex-1 min-w-0">
<div className={`text-[13px] font-semibold font-korean ${menu.enabled ? 'text-text-1' : 'text-text-3'}`}>
{menu.label}
</div>
<div className="text-[10px] text-text-3 font-mono">{menu.id}</div>
</div>
<button
onClick={() => onEditStart(menu.id)}
className="shrink-0 w-7 h-7 rounded border border-border bg-bg-2 text-text-3 text-[11px] flex items-center justify-center hover:bg-bg-hover hover:text-text-1 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-primary-cyan' : 'bg-bg-3 border border-border'
}`}
>
<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-border bg-bg-2 text-text-3 text-xs flex items-center justify-center hover:bg-bg-hover hover:text-text-1 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-border bg-bg-2 text-text-3 text-xs flex items-center justify-center hover:bg-bg-hover hover:text-text-1 transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
</button>
</div>
</div>
</div>
)
}
export default SortableMenuItem