8b8186d1c0
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m7s
신규 / 마이그된 패널: - 데이터 조회/선택: InvDataConfigPanel (wrapper) + InvRepeaterConfigPanel (V2Repeater 2029줄 폐기 → cp 신규 본체) - 통합 컴포넌트 7개 in-place cp: button / container / divider / search / stats / table / title - 옛 v2-* hidden 호환: InvDividerConfigPanel / InvTextConfigPanel / InvButtonConfigPanel cp 인프라: - _shared/cp/CPExtras.tsx 신규 — 8 공용 컴포넌트 (Hint / DimText / InlineLoader / SectionLabel / CpChip / ChipPickerBox / FeatureChipGrid / CPVisualGrid) - FeatureChipGrid: portal tooltip + Stripe 패턴 group cooldown (500ms delay → 즉시 전환) - CPVisualGrid: 시각 미리보기 카드 (두께/색/사이즈/시맨틱 등 진짜 의미 preview) - cp.css 다크 luminance 분리 + 키프레임 / CPPrimitives CPSelect 자동 검색·정렬 옛 V2 패널 4개 삭제: - V2RepeaterConfigPanel (2029) / V2ButtonConfigPanel (2212) / V2DividerLineConfigPanel (236) / V2TextDisplayConfigPanel (304) 설계 노트 5개 추가 (notes/gbpark/2026-04-28-*.md): - cp-panel-standard / cp-panel-day2-html-v4-match / invdata-inventory / inv-repeater-redesign / inv-naming-consolidation 다음: Inv* 일괄 네이밍 통합 + dead code 정리 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { ComponentStyle } from "@/types/screen";
|
|
import { ColorPickerWithTransparent } from "./common/ColorPickerWithTransparent";
|
|
import {
|
|
CPSection,
|
|
CPRow,
|
|
CPText,
|
|
CPSelect,
|
|
} from "@/components/v2/config-panels/_shared/cp";
|
|
|
|
interface StyleEditorProps {
|
|
style: ComponentStyle;
|
|
onStyleChange: (style: ComponentStyle) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) {
|
|
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style || {});
|
|
|
|
useEffect(() => {
|
|
setLocalStyle(style || {});
|
|
}, [style]);
|
|
|
|
const handleStyleChange = (property: keyof ComponentStyle, value: any) => {
|
|
const newStyle = { ...localStyle, [property]: value };
|
|
setLocalStyle(newStyle);
|
|
onStyleChange(newStyle);
|
|
};
|
|
|
|
// 숫자만 입력했을 때 자동으로 px 붙여주기
|
|
const handlePxBlur = (property: keyof ComponentStyle) => {
|
|
const val = localStyle[property];
|
|
if (val && /^\d+(\.\d+)?$/.test(String(val))) {
|
|
handleStyleChange(property, `${val}px`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={className}>
|
|
{/* 테두리 */}
|
|
<CPSection title="테두리">
|
|
<CPRow label="두께">
|
|
<CPText
|
|
mono
|
|
value={localStyle.borderWidth || ""}
|
|
onChange={(v) => handleStyleChange("borderWidth", v)}
|
|
onBlur={() => handlePxBlur("borderWidth")}
|
|
placeholder="1px"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="스타일">
|
|
<CPSelect
|
|
value={localStyle.borderStyle || "solid"}
|
|
onChange={(v) => handleStyleChange("borderStyle", v)}
|
|
>
|
|
<option value="solid">실선</option>
|
|
<option value="dashed">파선</option>
|
|
<option value="dotted">점선</option>
|
|
<option value="none">없음</option>
|
|
</CPSelect>
|
|
</CPRow>
|
|
<CPRow label="색상">
|
|
<ColorPickerWithTransparent
|
|
value={localStyle.borderColor}
|
|
onChange={(value) => handleStyleChange("borderColor", value)}
|
|
defaultColor="#e5e7eb"
|
|
placeholder="#e5e7eb"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="모서리">
|
|
<CPText
|
|
mono
|
|
value={localStyle.borderRadius || ""}
|
|
onChange={(v) => handleStyleChange("borderRadius", v)}
|
|
onBlur={() => handlePxBlur("borderRadius")}
|
|
placeholder="5px"
|
|
/>
|
|
</CPRow>
|
|
</CPSection>
|
|
|
|
{/* 배경 */}
|
|
<CPSection title="배경">
|
|
<CPRow label="색상">
|
|
<ColorPickerWithTransparent
|
|
value={localStyle.backgroundColor}
|
|
onChange={(value) => handleStyleChange("backgroundColor", value)}
|
|
defaultColor="#ffffff"
|
|
placeholder="#ffffff"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="이미지" help="CSS url() — 고급 사용자 전용">
|
|
<CPText
|
|
value={localStyle.backgroundImage || ""}
|
|
onChange={(v) => handleStyleChange("backgroundImage", v)}
|
|
placeholder="url('image.jpg')"
|
|
/>
|
|
</CPRow>
|
|
</CPSection>
|
|
|
|
{/* 텍스트 */}
|
|
<CPSection title="텍스트">
|
|
<CPRow label="색상">
|
|
<ColorPickerWithTransparent
|
|
value={localStyle.color}
|
|
onChange={(value) => handleStyleChange("color", value)}
|
|
defaultColor="#000000"
|
|
placeholder="#000000"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="크기">
|
|
<CPText
|
|
mono
|
|
value={localStyle.fontSize || ""}
|
|
onChange={(v) => handleStyleChange("fontSize", v)}
|
|
onBlur={() => handlePxBlur("fontSize")}
|
|
placeholder="14px"
|
|
/>
|
|
</CPRow>
|
|
<CPRow label="굵기">
|
|
<CPSelect
|
|
value={localStyle.fontWeight || "normal"}
|
|
onChange={(v) => handleStyleChange("fontWeight", v)}
|
|
>
|
|
<option value="normal">보통</option>
|
|
<option value="bold">굵게</option>
|
|
<option value="100">100</option>
|
|
<option value="400">400</option>
|
|
<option value="500">500</option>
|
|
<option value="600">600</option>
|
|
<option value="700">700</option>
|
|
</CPSelect>
|
|
</CPRow>
|
|
<CPRow label="정렬">
|
|
<CPSelect
|
|
value={localStyle.textAlign || "left"}
|
|
onChange={(v) => handleStyleChange("textAlign", v)}
|
|
>
|
|
<option value="left">왼쪽</option>
|
|
<option value="center">가운데</option>
|
|
<option value="right">오른쪽</option>
|
|
<option value="justify">양쪽</option>
|
|
</CPSelect>
|
|
</CPRow>
|
|
</CPSection>
|
|
</div>
|
|
);
|
|
}
|