2025-10-15 10:24:33 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2026-01-05 13:28:11 +09:00
|
|
|
|
import React, { useState } from "react";
|
2025-10-15 10:24:33 +09:00
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-01-05 13:28:11 +09:00
|
|
|
|
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,
|
2026-01-15 09:22:31 +09:00
|
|
|
|
Languages,
|
|
|
|
|
|
Settings2,
|
2026-01-16 09:59:16 +09:00
|
|
|
|
PanelLeft,
|
|
|
|
|
|
PanelLeftClose,
|
2026-01-05 13:28:11 +09:00
|
|
|
|
} from "lucide-react";
|
|
|
|
|
|
import { ScreenResolution, SCREEN_RESOLUTIONS } from "@/types/screen";
|
|
|
|
|
|
import {
|
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
|
DropdownMenuLabel,
|
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Popover,
|
|
|
|
|
|
PopoverContent,
|
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
|
|
|
|
|
|
|
interface GridSettings {
|
|
|
|
|
|
columns: number;
|
|
|
|
|
|
gap: number;
|
|
|
|
|
|
padding: number;
|
|
|
|
|
|
snapToGrid: boolean;
|
|
|
|
|
|
showGrid: boolean;
|
|
|
|
|
|
gridColor?: string;
|
|
|
|
|
|
gridOpacity?: number;
|
|
|
|
|
|
}
|
2025-10-15 10:24:33 +09:00
|
|
|
|
|
|
|
|
|
|
interface SlimToolbarProps {
|
|
|
|
|
|
screenName?: string;
|
|
|
|
|
|
tableName?: string;
|
2025-10-15 10:44:05 +09:00
|
|
|
|
screenResolution?: ScreenResolution;
|
2025-10-15 10:24:33 +09:00
|
|
|
|
onBack: () => void;
|
|
|
|
|
|
onSave: () => void;
|
|
|
|
|
|
isSaving?: boolean;
|
2025-10-17 10:12:41 +09:00
|
|
|
|
onPreview?: () => void;
|
2026-01-05 13:28:11 +09:00
|
|
|
|
onResolutionChange?: (resolution: ScreenResolution) => void;
|
|
|
|
|
|
gridSettings?: GridSettings;
|
|
|
|
|
|
onGridSettingsChange?: (settings: GridSettings) => void;
|
2026-01-14 10:20:27 +09:00
|
|
|
|
onGenerateMultilang?: () => void;
|
|
|
|
|
|
isGeneratingMultilang?: boolean;
|
2026-01-14 11:51:24 +09:00
|
|
|
|
onOpenMultilangSettings?: () => void;
|
2026-01-16 09:59:16 +09:00
|
|
|
|
// 패널 토글 기능
|
|
|
|
|
|
isPanelOpen?: boolean;
|
|
|
|
|
|
onTogglePanel?: () => void;
|
2025-10-15 10:24:33 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
|
|
|
|
|
screenName,
|
|
|
|
|
|
tableName,
|
2025-10-15 10:44:05 +09:00
|
|
|
|
screenResolution,
|
2025-10-15 10:24:33 +09:00
|
|
|
|
onBack,
|
|
|
|
|
|
onSave,
|
|
|
|
|
|
isSaving = false,
|
2025-10-17 10:12:41 +09:00
|
|
|
|
onPreview,
|
2026-01-05 13:28:11 +09:00
|
|
|
|
onResolutionChange,
|
|
|
|
|
|
gridSettings,
|
|
|
|
|
|
onGridSettingsChange,
|
2026-01-14 10:20:27 +09:00
|
|
|
|
onGenerateMultilang,
|
|
|
|
|
|
isGeneratingMultilang = false,
|
2026-01-14 11:51:24 +09:00
|
|
|
|
onOpenMultilangSettings,
|
2026-01-16 09:59:16 +09:00
|
|
|
|
isPanelOpen = false,
|
|
|
|
|
|
onTogglePanel,
|
2025-10-15 10:24:33 +09:00
|
|
|
|
}) => {
|
2026-01-05 13:28:11 +09:00
|
|
|
|
// 사용자 정의 해상도 상태
|
|
|
|
|
|
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-blue-600" />;
|
|
|
|
|
|
case "tablet":
|
|
|
|
|
|
return <Tablet className="h-4 w-4 text-green-600" />;
|
|
|
|
|
|
case "mobile":
|
|
|
|
|
|
return <Smartphone className="h-4 w-4 text-purple-600" />;
|
|
|
|
|
|
default:
|
|
|
|
|
|
return <Monitor className="h-4 w-4 text-blue-600" />;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-15 10:24:33 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-14 items-center justify-between border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white px-4 shadow-sm">
|
2026-01-16 09:59:16 +09:00
|
|
|
|
{/* 좌측: 네비게이션 + 패널 토글 + 화면 정보 */}
|
2025-10-15 10:24:33 +09:00
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={onBack} className="flex items-center space-x-2">
|
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
|
<span>목록으로</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
2026-01-16 09:59:16 +09:00
|
|
|
|
{onTogglePanel && <div className="h-6 w-px bg-gray-300" />}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 패널 토글 버튼 */}
|
|
|
|
|
|
{onTogglePanel && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant={isPanelOpen ? "default" : "outline"}
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={onTogglePanel}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
title="패널 열기/닫기 (P)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isPanelOpen ? (
|
|
|
|
|
|
<PanelLeftClose className="h-4 w-4" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<PanelLeft className="h-4 w-4" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span>패널</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-15 10:24:33 +09:00
|
|
|
|
<div className="h-6 w-px bg-gray-300" />
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h1 className="text-lg font-semibold text-gray-900">{screenName || "화면 설계"}</h1>
|
|
|
|
|
|
{tableName && (
|
|
|
|
|
|
<div className="mt-0.5 flex items-center space-x-1">
|
|
|
|
|
|
<Database className="h-3 w-3 text-gray-500" />
|
|
|
|
|
|
<span className="font-mono text-xs text-gray-500">{tableName}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-15 10:44:05 +09:00
|
|
|
|
|
2026-01-05 13:28:11 +09:00
|
|
|
|
{/* 해상도 선택 드롭다운 */}
|
2025-10-15 10:44:05 +09:00
|
|
|
|
{screenResolution && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-300" />
|
2026-01-05 13:28:11 +09:00
|
|
|
|
<Popover open={showCustomInput} onOpenChange={setShowCustomInput}>
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
|
<button className="flex items-center space-x-2 rounded-md bg-blue-50 px-3 py-1.5 transition-colors hover:bg-blue-100">
|
|
|
|
|
|
{getCategoryIcon(screenResolution.category || "desktop")}
|
|
|
|
|
|
<span className="text-sm font-medium text-blue-900">{screenResolution.name}</span>
|
|
|
|
|
|
<span className="text-xs text-blue-600">
|
|
|
|
|
|
({screenResolution.width} × {screenResolution.height})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{onResolutionChange && <ChevronDown className="h-3 w-3 text-blue-600" />}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
|
{onResolutionChange && (
|
|
|
|
|
|
<DropdownMenuContent align="start" className="w-64">
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs text-gray-500">데스크톱</DropdownMenuLabel>
|
|
|
|
|
|
{SCREEN_RESOLUTIONS.filter((r) => r.category === "desktop").map((resolution) => (
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
key={resolution.name}
|
|
|
|
|
|
onClick={() => onResolutionChange(resolution)}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Monitor className="h-4 w-4 text-blue-600" />
|
|
|
|
|
|
<span className="flex-1">{resolution.name}</span>
|
|
|
|
|
|
<span className="text-xs text-gray-400">
|
|
|
|
|
|
{resolution.width}×{resolution.height}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs text-gray-500">태블릿</DropdownMenuLabel>
|
|
|
|
|
|
{SCREEN_RESOLUTIONS.filter((r) => r.category === "tablet").map((resolution) => (
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
key={resolution.name}
|
|
|
|
|
|
onClick={() => onResolutionChange(resolution)}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tablet className="h-4 w-4 text-green-600" />
|
|
|
|
|
|
<span className="flex-1">{resolution.name}</span>
|
|
|
|
|
|
<span className="text-xs text-gray-400">
|
|
|
|
|
|
{resolution.width}×{resolution.height}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs text-gray-500">모바일</DropdownMenuLabel>
|
|
|
|
|
|
{SCREEN_RESOLUTIONS.filter((r) => r.category === "mobile").map((resolution) => (
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
key={resolution.name}
|
|
|
|
|
|
onClick={() => onResolutionChange(resolution)}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Smartphone className="h-4 w-4 text-purple-600" />
|
|
|
|
|
|
<span className="flex-1">{resolution.name}</span>
|
|
|
|
|
|
<span className="text-xs text-gray-400">
|
|
|
|
|
|
{resolution.width}×{resolution.height}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
<DropdownMenuLabel className="text-xs text-gray-500">사용자 정의</DropdownMenuLabel>
|
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setCustomWidth(screenResolution.width.toString());
|
|
|
|
|
|
setCustomHeight(screenResolution.height.toString());
|
|
|
|
|
|
setShowCustomInput(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings className="h-4 w-4 text-gray-600" />
|
|
|
|
|
|
<span className="flex-1">사용자 정의...</span>
|
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
<PopoverContent align="start" className="w-64 p-3">
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="text-sm font-medium">사용자 정의 해상도</div>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs text-gray-500">너비 (px)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={customWidth}
|
|
|
|
|
|
onChange={(e) => setCustomWidth(e.target.value)}
|
|
|
|
|
|
placeholder="1920"
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs text-gray-500">높이 (px)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={customHeight}
|
|
|
|
|
|
onChange={(e) => setCustomHeight(e.target.value)}
|
|
|
|
|
|
placeholder="1080"
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button onClick={handleCustomResolution} size="sm" className="w-full">
|
|
|
|
|
|
적용
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
|
</Popover>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 격자 설정 */}
|
|
|
|
|
|
{gridSettings && onGridSettingsChange && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-300" />
|
|
|
|
|
|
<div className="flex items-center space-x-2 rounded-md bg-gray-50 px-3 py-1.5">
|
|
|
|
|
|
<Grid3X3 className="h-4 w-4 text-gray-600" />
|
|
|
|
|
|
<div className="flex items-center space-x-3">
|
|
|
|
|
|
<label className="flex cursor-pointer items-center space-x-1.5">
|
|
|
|
|
|
{gridSettings.showGrid ? (
|
|
|
|
|
|
<Eye className="h-3.5 w-3.5 text-primary" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<EyeOff className="h-3.5 w-3.5 text-gray-400" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={gridSettings.showGrid}
|
|
|
|
|
|
onCheckedChange={(checked) => updateGridSetting("showGrid", checked as boolean)}
|
|
|
|
|
|
className="h-3.5 w-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-gray-600">격자 표시</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="flex cursor-pointer items-center space-x-1.5">
|
|
|
|
|
|
<Zap className={`h-3.5 w-3.5 ${gridSettings.snapToGrid ? "text-primary" : "text-gray-400"}`} />
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={gridSettings.snapToGrid}
|
|
|
|
|
|
onCheckedChange={(checked) => updateGridSetting("snapToGrid", checked as boolean)}
|
|
|
|
|
|
className="h-3.5 w-3.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-xs text-gray-600">격자 스냅</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2025-10-15 10:44:05 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-10-15 10:24:33 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-10-17 10:12:41 +09:00
|
|
|
|
{/* 우측: 버튼들 */}
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
{onPreview && (
|
|
|
|
|
|
<Button variant="outline" onClick={onPreview} className="flex items-center space-x-2">
|
|
|
|
|
|
<Smartphone className="h-4 w-4" />
|
|
|
|
|
|
<span>반응형 미리보기</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2026-01-14 10:20:27 +09:00
|
|
|
|
{onGenerateMultilang && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={onGenerateMultilang}
|
|
|
|
|
|
disabled={isGeneratingMultilang}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
title="화면 라벨에 대한 다국어 키를 자동으로 생성합니다"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Languages className="h-4 w-4" />
|
|
|
|
|
|
<span>{isGeneratingMultilang ? "생성 중..." : "다국어 생성"}</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2026-01-14 11:51:24 +09:00
|
|
|
|
{onOpenMultilangSettings && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={onOpenMultilangSettings}
|
|
|
|
|
|
className="flex items-center space-x-2"
|
|
|
|
|
|
title="다국어 키 연결 및 설정을 관리합니다"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings2 className="h-4 w-4" />
|
|
|
|
|
|
<span>다국어 설정</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
2025-10-15 10:24:33 +09:00
|
|
|
|
<Button onClick={onSave} disabled={isSaving} className="flex items-center space-x-2">
|
|
|
|
|
|
<Save className="h-4 w-4" />
|
|
|
|
|
|
<span>{isSaving ? "저장 중..." : "저장"}</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default SlimToolbar;
|