ERP-node/frontend/components/screen/toolbar/SlimToolbar.tsx

510 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import {
Database,
ArrowLeft,
Save,
Monitor,
Smartphone,
Tablet,
ChevronDown,
Settings,
Grid3X3,
Eye,
EyeOff,
Zap,
Languages,
Settings2,
PanelLeft,
PanelLeftClose,
AlignStartVertical,
AlignCenterVertical,
AlignEndVertical,
AlignStartHorizontal,
AlignCenterHorizontal,
AlignEndHorizontal,
AlignHorizontalSpaceAround,
AlignVerticalSpaceAround,
RulerIcon,
Tag,
Keyboard,
Equal,
} from "lucide-react";
import { ScreenResolution, SCREEN_RESOLUTIONS } from "@/types/screen";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
interface GridSettings {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
showGrid: boolean;
gridColor?: string;
gridOpacity?: number;
}
type AlignMode = "left" | "centerX" | "right" | "top" | "centerY" | "bottom";
type DistributeDirection = "horizontal" | "vertical";
type MatchSizeMode = "width" | "height" | "both";
interface SlimToolbarProps {
screenName?: string;
tableName?: string;
screenResolution?: ScreenResolution;
onBack: () => void;
onSave: () => void;
isSaving?: boolean;
onMigrateToV3?: () => void;
isMigrating?: boolean;
onPreview?: () => void;
onResolutionChange?: (resolution: ScreenResolution) => void;
gridSettings?: GridSettings;
onGridSettingsChange?: (settings: GridSettings) => void;
onGenerateMultilang?: () => void;
isGeneratingMultilang?: boolean;
onOpenMultilangSettings?: () => void;
// 패널 토글 기능
isPanelOpen?: boolean;
onTogglePanel?: () => void;
// 정렬/배분/크기 기능
selectedCount?: number;
onAlign?: (mode: AlignMode) => void;
onDistribute?: (direction: DistributeDirection) => void;
onMatchSize?: (mode: MatchSizeMode) => void;
onToggleLabels?: () => void;
onShowShortcuts?: () => void;
}
export const SlimToolbar: React.FC<SlimToolbarProps> = ({
screenName,
tableName,
screenResolution,
onBack,
onSave,
isSaving = false,
onMigrateToV3,
isMigrating = false,
onPreview,
onResolutionChange,
gridSettings,
onGridSettingsChange,
onGenerateMultilang,
isGeneratingMultilang = false,
onOpenMultilangSettings,
isPanelOpen = false,
onTogglePanel,
selectedCount = 0,
onAlign,
onDistribute,
onMatchSize,
onToggleLabels,
onShowShortcuts,
}) => {
// 사용자 정의 해상도 상태
const [customWidth, setCustomWidth] = useState("");
const [customHeight, setCustomHeight] = useState("");
const [showCustomInput, setShowCustomInput] = useState(false);
const getCategoryIcon = (category: string) => {
switch (category) {
case "desktop":
return <Monitor className="h-4 w-4 text-primary" />;
case "tablet":
return <Tablet className="h-4 w-4 text-green-500" />;
case "mobile":
return <Smartphone className="h-4 w-4 text-purple-500" />;
default:
return <Monitor className="h-4 w-4 text-primary" />;
}
};
const handleCustomResolution = () => {
const width = parseInt(customWidth);
const height = parseInt(customHeight);
if (width > 0 && height > 0 && onResolutionChange) {
const customResolution: ScreenResolution = {
width,
height,
name: `사용자 정의 (${width}×${height})`,
category: "custom",
};
onResolutionChange(customResolution);
setShowCustomInput(false);
}
};
const updateGridSetting = (key: keyof GridSettings, value: boolean) => {
if (onGridSettingsChange && gridSettings) {
onGridSettingsChange({
...gridSettings,
[key]: value,
});
}
};
return (
<div className="flex h-12 items-center justify-between border-b bg-background px-4 shadow-sm">
{/* 좌측: 네비게이션 + 패널 토글 + 화면 정보 */}
<div className="flex items-center gap-3">
{/* 네비게이션 그룹 */}
<Button variant="ghost" size="sm" onClick={onBack} className="flex items-center gap-2">
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button>
{onTogglePanel && <div className="h-5 w-px bg-border" />}
{/* 뷰 컨트롤 그룹 */}
{onTogglePanel && (
<Button
variant={isPanelOpen ? "default" : "outline"}
size="sm"
onClick={onTogglePanel}
className="flex items-center gap-2"
title="패널 열기/닫기 (P)"
>
{isPanelOpen ? (
<PanelLeftClose className="h-4 w-4" />
) : (
<PanelLeft className="h-4 w-4" />
)}
<span className="hidden sm:inline"></span>
</Button>
)}
<div className="h-5 w-px bg-border" />
{/* 화면 정보 */}
<div className="flex items-center gap-3">
<div>
<h1 className="text-base font-semibold text-foreground sm:text-lg">{screenName || "화면 설계"}</h1>
{tableName && (
<div className="mt-0.5 flex items-center gap-1">
<Database className="h-3 w-3 text-muted-foreground" />
<span className="font-mono text-xs text-muted-foreground">{tableName}</span>
</div>
)}
</div>
</div>
{/* 해상도 도구 그룹 */}
{screenResolution && (
<>
<div className="h-5 w-px bg-border" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded-md border bg-secondary/50 px-3 py-1.5 text-xs transition-colors hover:bg-secondary sm:text-sm">
{getCategoryIcon(screenResolution.category || "desktop")}
<span className="hidden font-medium text-foreground sm:inline">{screenResolution.name}</span>
<span className="text-muted-foreground">
({screenResolution.width} × {screenResolution.height})
</span>
{onResolutionChange && <ChevronDown className="h-3 w-3 text-muted-foreground" />}
</button>
</DropdownMenuTrigger>
{onResolutionChange && (
<DropdownMenuContent align="start" className="w-64">
<DropdownMenuLabel className="text-xs text-muted-foreground"></DropdownMenuLabel>
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
<DropdownMenuItem
key={resolution.name}
onClick={() => onResolutionChange(resolution)}
className="flex items-center gap-2"
>
<Monitor className="h-4 w-4 text-primary" />
<span className="flex-1">{resolution.name}</span>
<span className="text-xs text-muted-foreground">
{resolution.width}×{resolution.height}
</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">릿</DropdownMenuLabel>
{SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => (
<DropdownMenuItem
key={resolution.name}
onClick={() => onResolutionChange(resolution)}
className="flex items-center gap-2"
>
<Tablet className="h-4 w-4 text-green-500" />
<span className="flex-1">{resolution.name}</span>
<span className="text-xs text-muted-foreground">
{resolution.width}×{resolution.height}
</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground"></DropdownMenuLabel>
{SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => (
<DropdownMenuItem
key={resolution.name}
onClick={() => onResolutionChange(resolution)}
className="flex items-center gap-2"
>
<Smartphone className="h-4 w-4 text-purple-500" />
<span className="flex-1">{resolution.name}</span>
<span className="text-xs text-muted-foreground">
{resolution.width}×{resolution.height}
</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground"> </DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
setCustomWidth(screenResolution.width.toString());
setCustomHeight(screenResolution.height.toString());
setShowCustomInput(true);
}}
className="flex items-center gap-2"
>
<Settings className="h-4 w-4 text-muted-foreground" />
<span className="flex-1"> ...</span>
</DropdownMenuItem>
</DropdownMenuContent>
)}
</DropdownMenu>
{/* 사용자 정의 해상도 다이얼로그 */}
<Dialog open={showCustomInput} onOpenChange={setShowCustomInput}>
<DialogContent className="sm:max-w-[320px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="customWidth" className="text-sm"> (px)</Label>
<Input
id="customWidth"
type="number"
value={customWidth}
onChange={(e) => setCustomWidth(e.target.value)}
placeholder="1920"
/>
</div>
<div className="space-y-2">
<Label htmlFor="customHeight" className="text-sm"> (px)</Label>
<Input
id="customHeight"
type="number"
value={customHeight}
onChange={(e) => setCustomHeight(e.target.value)}
placeholder="1080"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCustomInput(false)}>
</Button>
<Button onClick={handleCustomResolution}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)}
{/* 격자 도구 그룹 */}
{gridSettings && onGridSettingsChange && (
<>
<div className="h-5 w-px bg-border" />
<div className="flex items-center gap-2 rounded-md bg-secondary/30 px-3 py-1.5">
<Grid3X3 className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-3">
<label className="flex cursor-pointer items-center gap-1.5">
{gridSettings.showGrid ? (
<Eye className="h-3.5 w-3.5 text-primary" />
) : (
<EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
)}
<Checkbox
checked={gridSettings.showGrid}
onCheckedChange={(checked) => updateGridSetting("showGrid", checked as boolean)}
className="h-3.5 w-3.5"
/>
<span className="hidden text-xs text-foreground sm:inline"></span>
</label>
<label className="flex cursor-pointer items-center gap-1.5">
<Zap className={`h-3.5 w-3.5 ${gridSettings.snapToGrid ? "text-primary" : "text-muted-foreground"}`} />
<Checkbox
checked={gridSettings.snapToGrid}
onCheckedChange={(checked) => updateGridSetting("snapToGrid", checked as boolean)}
className="h-3.5 w-3.5"
/>
<span className="hidden text-xs text-foreground sm:inline"></span>
</label>
</div>
</div>
</>
)}
</div>
{/* 중앙: 정렬/배분 도구 (다중 선택 시) */}
{selectedCount >= 2 && (onAlign || onDistribute || onMatchSize) && (
<div className="flex items-center gap-1 rounded-md border bg-accent/50 px-2 py-1">
{/* 정렬 */}
{onAlign && (
<>
<span className="mr-1 hidden text-xs font-medium text-foreground sm:inline"></span>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("left")} title="좌측 정렬 (Alt+L)">
<AlignStartVertical className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("centerX")} title="가로 중앙 (Alt+C)">
<AlignCenterVertical className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("right")} title="우측 정렬 (Alt+R)">
<AlignEndVertical className="h-3.5 w-3.5" />
</Button>
<div className="mx-0.5 h-4 w-px bg-border" />
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("top")} title="상단 정렬 (Alt+T)">
<AlignStartHorizontal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("centerY")} title="세로 중앙 (Alt+M)">
<AlignCenterHorizontal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onAlign("bottom")} title="하단 정렬 (Alt+B)">
<AlignEndHorizontal className="h-3.5 w-3.5" />
</Button>
</>
)}
{/* 배분 (3개 이상) */}
{onDistribute && selectedCount >= 3 && (
<>
<div className="mx-1 h-4 w-px bg-border" />
<span className="mr-1 hidden text-xs font-medium text-foreground sm:inline"></span>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onDistribute("horizontal")} title="가로 균등 배분 (Alt+H)">
<AlignHorizontalSpaceAround className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onDistribute("vertical")} title="세로 균등 배분 (Alt+V)">
<AlignVerticalSpaceAround className="h-3.5 w-3.5" />
</Button>
</>
)}
{/* 크기 맞추기 */}
{onMatchSize && (
<>
<div className="mx-1 h-4 w-px bg-border" />
<span className="mr-1 hidden text-xs font-medium text-foreground sm:inline"></span>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onMatchSize("width")} title="너비 맞추기 (Alt+W)">
<RulerIcon className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onMatchSize("height")} title="높이 맞추기 (Alt+E)">
<RulerIcon className="h-3.5 w-3.5 rotate-90" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => onMatchSize("both")} title="크기 모두 맞추기">
<Equal className="h-3.5 w-3.5" />
</Button>
</>
)}
<div className="mx-1 h-4 w-px bg-border" />
<span className="text-xs font-medium text-muted-foreground">{selectedCount}</span>
</div>
)}
{/* 우측: 액션 버튼 그룹 */}
<div className="flex items-center gap-2">
{/* 라벨 토글 */}
{onToggleLabels && (
<Button
variant="outline"
size="sm"
onClick={onToggleLabels}
className="hidden items-center gap-1 sm:flex"
title="라벨 일괄 표시/숨기기 (Alt+Shift+L)"
>
<Tag className="h-4 w-4" />
<span></span>
</Button>
)}
{/* 단축키 도움말 */}
{onShowShortcuts && (
<Button
variant="ghost"
size="icon"
onClick={onShowShortcuts}
className="h-9 w-9"
title="단축키 도움말 (?)"
>
<Keyboard className="h-4 w-4" />
</Button>
)}
{onPreview && (
<Button variant="outline" size="sm" onClick={onPreview} className="hidden items-center gap-2 sm:flex">
<Eye className="h-4 w-4" />
<span></span>
</Button>
)}
{onGenerateMultilang && (
<Button
variant="outline"
size="sm"
onClick={onGenerateMultilang}
disabled={isGeneratingMultilang}
className="hidden items-center gap-2 sm:flex"
title="화면 라벨에 대한 다국어 키를 자동으로 생성합니다"
>
<Languages className="h-4 w-4" />
<span>{isGeneratingMultilang ? "생성 중..." : "다국어"}</span>
</Button>
)}
{onOpenMultilangSettings && (
<Button
variant="outline"
size="sm"
onClick={onOpenMultilangSettings}
className="hidden items-center gap-2 sm:flex"
title="다국어 키 연결 및 설정을 관리합니다"
>
<Settings2 className="h-4 w-4" />
<span></span>
</Button>
)}
{onMigrateToV3 && (
<Button
variant="default"
size="sm"
onClick={onMigrateToV3}
disabled={isMigrating}
className="hidden items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white shadow-sm hover:from-indigo-600 hover:to-purple-700 sm:flex"
>
<Zap className="h-4 w-4" />
<span>{isMigrating ? "변환 중..." : "V3 변환"}</span>
</Button>
)}
<Button onClick={onSave} disabled={isSaving} size="sm" className="flex items-center gap-2">
<Save className="h-4 w-4" />
<span className="hidden sm:inline">{isSaving ? "저장 중..." : "저장"}</span>
</Button>
</div>
</div>
);
};
export default SlimToolbar;