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>
162 lines
6.3 KiB
TypeScript
162 lines
6.3 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-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
|