426 lines
17 KiB
TypeScript
426 lines
17 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from "react";
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Switch } from "@/components/ui/switch";
|
||
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Code, Monitor, Moon, Sun } from "lucide-react";
|
||
|
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
||
|
|
import { WidgetComponent, CodeTypeConfig } from "@/types/screen";
|
||
|
|
|
||
|
|
export const CodeConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||
|
|
component,
|
||
|
|
onUpdateComponent,
|
||
|
|
onUpdateProperty,
|
||
|
|
}) => {
|
||
|
|
const widget = component as WidgetComponent;
|
||
|
|
const config = (widget.webTypeConfig as CodeTypeConfig) || {};
|
||
|
|
|
||
|
|
// 로컬 상태
|
||
|
|
const [localConfig, setLocalConfig] = useState<CodeTypeConfig>({
|
||
|
|
language: config.language || "javascript",
|
||
|
|
theme: config.theme || "light",
|
||
|
|
showLineNumbers: config.showLineNumbers !== false, // 기본값 true
|
||
|
|
wordWrap: config.wordWrap || false,
|
||
|
|
fontSize: config.fontSize || 14,
|
||
|
|
tabSize: config.tabSize || 2,
|
||
|
|
readOnly: config.readOnly || false,
|
||
|
|
showMinimap: config.showMinimap || false,
|
||
|
|
autoComplete: config.autoComplete !== false, // 기본값 true
|
||
|
|
bracketMatching: config.bracketMatching !== false, // 기본값 true
|
||
|
|
defaultValue: config.defaultValue || "",
|
||
|
|
placeholder: config.placeholder || "코드를 입력하세요...",
|
||
|
|
height: config.height || 300,
|
||
|
|
required: config.required || false,
|
||
|
|
});
|
||
|
|
|
||
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||
|
|
useEffect(() => {
|
||
|
|
const currentConfig = (widget.webTypeConfig as CodeTypeConfig) || {};
|
||
|
|
setLocalConfig({
|
||
|
|
language: currentConfig.language || "javascript",
|
||
|
|
theme: currentConfig.theme || "light",
|
||
|
|
showLineNumbers: currentConfig.showLineNumbers !== false,
|
||
|
|
wordWrap: currentConfig.wordWrap || false,
|
||
|
|
fontSize: currentConfig.fontSize || 14,
|
||
|
|
tabSize: currentConfig.tabSize || 2,
|
||
|
|
readOnly: currentConfig.readOnly || false,
|
||
|
|
showMinimap: currentConfig.showMinimap || false,
|
||
|
|
autoComplete: currentConfig.autoComplete !== false,
|
||
|
|
bracketMatching: currentConfig.bracketMatching !== false,
|
||
|
|
defaultValue: currentConfig.defaultValue || "",
|
||
|
|
placeholder: currentConfig.placeholder || "코드를 입력하세요...",
|
||
|
|
height: currentConfig.height || 300,
|
||
|
|
required: currentConfig.required || false,
|
||
|
|
});
|
||
|
|
}, [widget.webTypeConfig]);
|
||
|
|
|
||
|
|
// 설정 업데이트 핸들러
|
||
|
|
const updateConfig = (field: keyof CodeTypeConfig, value: any) => {
|
||
|
|
const newConfig = { ...localConfig, [field]: value };
|
||
|
|
setLocalConfig(newConfig);
|
||
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 지원되는 언어 목록
|
||
|
|
const supportedLanguages = [
|
||
|
|
{ value: "javascript", label: "JavaScript", sample: "console.log('Hello World');" },
|
||
|
|
{ value: "typescript", label: "TypeScript", sample: "const message: string = 'Hello World';" },
|
||
|
|
{ value: "python", label: "Python", sample: "print('Hello World')" },
|
||
|
|
{ value: "java", label: "Java", sample: "System.out.println('Hello World');" },
|
||
|
|
{ value: "html", label: "HTML", sample: "<h1>Hello World</h1>" },
|
||
|
|
{ value: "css", label: "CSS", sample: "body { color: #333; }" },
|
||
|
|
{ value: "sql", label: "SQL", sample: "SELECT * FROM users;" },
|
||
|
|
{ value: "json", label: "JSON", sample: '{"message": "Hello World"}' },
|
||
|
|
{ value: "xml", label: "XML", sample: "<message>Hello World</message>" },
|
||
|
|
{ value: "markdown", label: "Markdown", sample: "# Hello World" },
|
||
|
|
{ value: "yaml", label: "YAML", sample: "message: Hello World" },
|
||
|
|
{ value: "shell", label: "Shell", sample: "echo 'Hello World'" },
|
||
|
|
{ value: "php", label: "PHP", sample: "<?php echo 'Hello World'; ?>" },
|
||
|
|
{ value: "go", label: "Go", sample: 'fmt.Println("Hello World")' },
|
||
|
|
{ value: "rust", label: "Rust", sample: 'println!("Hello World");' },
|
||
|
|
{ value: "plaintext", label: "Plain Text", sample: "Hello World" },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 테마 목록
|
||
|
|
const themes = [
|
||
|
|
{ value: "light", label: "Light", icon: Sun },
|
||
|
|
{ value: "dark", label: "Dark", icon: Moon },
|
||
|
|
{ value: "vs", label: "Visual Studio", icon: Monitor },
|
||
|
|
{ value: "github", label: "GitHub", icon: Monitor },
|
||
|
|
{ value: "monokai", label: "Monokai", icon: Monitor },
|
||
|
|
{ value: "solarized", label: "Solarized", icon: Monitor },
|
||
|
|
];
|
||
|
|
|
||
|
|
// 샘플 코드 설정
|
||
|
|
const setSampleCode = () => {
|
||
|
|
const selectedLang = supportedLanguages.find((lang) => lang.value === localConfig.language);
|
||
|
|
if (selectedLang) {
|
||
|
|
updateConfig("defaultValue", selectedLang.sample);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
||
|
|
<Code className="h-4 w-4" />
|
||
|
|
코드 에디터 설정
|
||
|
|
</CardTitle>
|
||
|
|
<CardDescription className="text-xs">코드 편집기의 언어, 테마, 편집 옵션을 설정합니다.</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
{/* 기본 설정 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">기본 설정</h4>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="language" className="text-xs">
|
||
|
|
프로그래밍 언어
|
||
|
|
</Label>
|
||
|
|
<Select
|
||
|
|
value={localConfig.language || "javascript"}
|
||
|
|
onValueChange={(value) => updateConfig("language", value)}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="text-xs">
|
||
|
|
<SelectValue placeholder="언어 선택" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{supportedLanguages.map((lang) => (
|
||
|
|
<SelectItem key={lang.value} value={lang.value}>
|
||
|
|
<div className="flex flex-col">
|
||
|
|
<span>{lang.label}</span>
|
||
|
|
<span className="text-muted-foreground font-mono text-xs">{lang.sample}</span>
|
||
|
|
</div>
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="theme" className="text-xs">
|
||
|
|
테마
|
||
|
|
</Label>
|
||
|
|
<Select value={localConfig.theme || "light"} onValueChange={(value) => updateConfig("theme", value)}>
|
||
|
|
<SelectTrigger className="text-xs">
|
||
|
|
<SelectValue placeholder="테마 선택" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{themes.map((theme) => (
|
||
|
|
<SelectItem key={theme.value} value={theme.value}>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<theme.icon className="h-3 w-3" />
|
||
|
|
{theme.label}
|
||
|
|
</div>
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="height" className="text-xs">
|
||
|
|
에디터 높이: {localConfig.height}px
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="height"
|
||
|
|
type="range"
|
||
|
|
min={150}
|
||
|
|
max={800}
|
||
|
|
step={50}
|
||
|
|
value={localConfig.height || 300}
|
||
|
|
onChange={(e) => updateConfig("height", parseInt(e.target.value))}
|
||
|
|
className="text-xs"
|
||
|
|
/>
|
||
|
|
<div className="text-muted-foreground flex justify-between text-xs">
|
||
|
|
<span>150px</span>
|
||
|
|
<span>800px</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 편집기 옵션 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">편집기 옵션</h4>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="fontSize" className="text-xs">
|
||
|
|
글꼴 크기
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="fontSize"
|
||
|
|
type="number"
|
||
|
|
value={localConfig.fontSize || 14}
|
||
|
|
onChange={(e) => updateConfig("fontSize", parseInt(e.target.value))}
|
||
|
|
min={10}
|
||
|
|
max={24}
|
||
|
|
className="text-xs"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="tabSize" className="text-xs">
|
||
|
|
탭 크기
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="tabSize"
|
||
|
|
type="number"
|
||
|
|
value={localConfig.tabSize || 2}
|
||
|
|
onChange={(e) => updateConfig("tabSize", parseInt(e.target.value))}
|
||
|
|
min={1}
|
||
|
|
max={8}
|
||
|
|
className="text-xs"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="showLineNumbers" className="text-xs">
|
||
|
|
줄 번호 표시
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">코드 라인의 번호를 표시합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="showLineNumbers"
|
||
|
|
checked={localConfig.showLineNumbers !== false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("showLineNumbers", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="wordWrap" className="text-xs">
|
||
|
|
줄 바꿈
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">긴 라인을 자동으로 줄바꿈합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="wordWrap"
|
||
|
|
checked={localConfig.wordWrap || false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("wordWrap", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="showMinimap" className="text-xs">
|
||
|
|
미니맵 표시
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">코드의 축소된 미리보기를 표시합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="showMinimap"
|
||
|
|
checked={localConfig.showMinimap || false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("showMinimap", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 고급 기능 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">고급 기능</h4>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="autoComplete" className="text-xs">
|
||
|
|
자동 완성
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">코드 자동 완성 기능을 활성화합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="autoComplete"
|
||
|
|
checked={localConfig.autoComplete !== false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("autoComplete", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="bracketMatching" className="text-xs">
|
||
|
|
괄호 매칭
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">매칭되는 괄호를 하이라이트합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="bracketMatching"
|
||
|
|
checked={localConfig.bracketMatching !== false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("bracketMatching", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 기본값 설정 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">기본값</h4>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="placeholder" className="text-xs">
|
||
|
|
플레이스홀더
|
||
|
|
</Label>
|
||
|
|
<Input
|
||
|
|
id="placeholder"
|
||
|
|
value={localConfig.placeholder || ""}
|
||
|
|
onChange={(e) => updateConfig("placeholder", e.target.value)}
|
||
|
|
placeholder="코드를 입력하세요..."
|
||
|
|
className="text-xs"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<Label htmlFor="defaultValue" className="text-xs">
|
||
|
|
기본 코드
|
||
|
|
</Label>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={setSampleCode}
|
||
|
|
className="text-xs text-blue-600 underline hover:text-blue-800"
|
||
|
|
>
|
||
|
|
샘플 코드 사용
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<Textarea
|
||
|
|
id="defaultValue"
|
||
|
|
value={localConfig.defaultValue || ""}
|
||
|
|
onChange={(e) => updateConfig("defaultValue", e.target.value)}
|
||
|
|
placeholder="기본 코드 내용"
|
||
|
|
className="font-mono text-xs"
|
||
|
|
rows={4}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 상태 설정 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">상태 설정</h4>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="readOnly" className="text-xs">
|
||
|
|
읽기 전용
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">코드를 수정할 수 없습니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="readOnly"
|
||
|
|
checked={localConfig.readOnly || false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("readOnly", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label htmlFor="required" className="text-xs">
|
||
|
|
필수 입력
|
||
|
|
</Label>
|
||
|
|
<p className="text-muted-foreground text-xs">코드가 입력되어야 합니다.</p>
|
||
|
|
</div>
|
||
|
|
<Switch
|
||
|
|
id="required"
|
||
|
|
checked={localConfig.required || false}
|
||
|
|
onCheckedChange={(checked) => updateConfig("required", checked)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 미리보기 */}
|
||
|
|
<div className="space-y-3">
|
||
|
|
<h4 className="text-sm font-medium">미리보기</h4>
|
||
|
|
<div className="bg-muted/50 rounded-md border p-3">
|
||
|
|
<div
|
||
|
|
className={`rounded border font-mono text-xs ${
|
||
|
|
localConfig.theme === "dark" ? "bg-gray-900 text-gray-100" : "bg-white text-gray-900"
|
||
|
|
}`}
|
||
|
|
style={{ height: `${Math.min(localConfig.height || 300, 200)}px` }}
|
||
|
|
>
|
||
|
|
<div className="flex items-center border-b bg-gray-50 px-3 py-1 text-gray-700">
|
||
|
|
<Code className="mr-2 h-3 w-3" />
|
||
|
|
<span className="text-xs">
|
||
|
|
{supportedLanguages.find((l) => l.value === localConfig.language)?.label || "JavaScript"}
|
||
|
|
</span>
|
||
|
|
{localConfig.showLineNumbers && <span className="ml-auto text-xs text-gray-500">줄번호</span>}
|
||
|
|
</div>
|
||
|
|
<div className="overflow-auto p-3" style={{ height: "calc(100% - 32px)" }}>
|
||
|
|
{localConfig.defaultValue ? (
|
||
|
|
<pre className="text-xs">
|
||
|
|
{localConfig.showLineNumbers && <span className="mr-3 text-gray-400 select-none">1</span>}
|
||
|
|
{localConfig.defaultValue}
|
||
|
|
</pre>
|
||
|
|
) : (
|
||
|
|
<div className="text-gray-400 italic">{localConfig.placeholder}</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="text-muted-foreground mt-2 space-y-1 text-xs">
|
||
|
|
<div>
|
||
|
|
언어: {supportedLanguages.find((l) => l.value === localConfig.language)?.label}• 테마:{" "}
|
||
|
|
{themes.find((t) => t.value === localConfig.theme)?.label}• 크기: {localConfig.fontSize}px
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
{localConfig.showLineNumbers && "줄번호 • "}
|
||
|
|
{localConfig.wordWrap && "줄바꿈 • "}
|
||
|
|
{localConfig.showMinimap && "미니맵 • "}
|
||
|
|
{localConfig.autoComplete && "자동완성 • "}
|
||
|
|
{localConfig.bracketMatching && "괄호매칭 • "}
|
||
|
|
{localConfig.readOnly && "읽기전용 • "}
|
||
|
|
{localConfig.required && "필수"}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
CodeConfigPanel.displayName = "CodeConfigPanel";
|
||
|
|
|
||
|
|
|