테두리 설정

This commit is contained in:
kjs
2025-09-01 17:05:36 +09:00
parent 984dd70505
commit ff2b3c37c6
4 changed files with 157 additions and 56 deletions

View File

@@ -36,16 +36,22 @@ interface RealtimePreviewProps {
// 웹 타입에 따른 위젯 렌더링
const renderWidget = (component: ComponentData) => {
const { widgetType, label, placeholder, required, readonly, columnName } = component;
const { widgetType, label, placeholder, required, readonly, columnName, style } = component;
// 디버깅: 실제 widgetType 값 확인
console.log("RealtimePreview - widgetType:", widgetType, "columnName:", columnName);
// 사용자가 테두리를 설정했는지 확인
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
// 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기
const borderClass = hasCustomBorder ? "!border-0" : "";
const commonProps = {
placeholder: placeholder || `입력하세요...`,
disabled: readonly,
required: required,
className: "w-full h-full",
className: `w-full h-full ${borderClass}`,
};
switch (widgetType) {
@@ -68,7 +74,9 @@ const renderWidget = (component: ComponentData) => {
<select
disabled={readonly}
required={required}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
className={`w-full rounded-md px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 ${
hasCustomBorder ? "!border-0" : "border border-gray-300"
}`}
>
<option value="">{placeholder || "선택하세요..."}</option>
<option value="option1"> 1</option>
@@ -130,7 +138,12 @@ const renderWidget = (component: ComponentData) => {
case "code":
return (
<Textarea {...commonProps} rows={4} className="w-full font-mono text-sm" placeholder="코드를 입력하세요..." />
<Textarea
{...commonProps}
rows={4}
className={`w-full font-mono text-sm ${borderClass}`}
placeholder="코드를 입력하세요..."
/>
);
case "entity":
@@ -138,7 +151,9 @@ const renderWidget = (component: ComponentData) => {
<select
disabled={readonly}
required={required}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
className={`w-full rounded-md px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 ${
hasCustomBorder ? "!border-0" : "border border-gray-300"
}`}
>
<option value=""> ...</option>
<option value="user"></option>
@@ -153,7 +168,9 @@ const renderWidget = (component: ComponentData) => {
type="file"
disabled={readonly}
required={required}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
className={`w-full rounded-md px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 ${
hasCustomBorder ? "!border-0" : "border border-gray-300"
}`}
/>
);
@@ -208,17 +225,34 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
}) => {
const { type, label, tableName, columnName, widgetType, size, style } = component;
// 사용자가 테두리를 설정했는지 확인
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
// 기본 선택 테두리는 사용자 테두리가 없을 때만 적용
const defaultRingClass = hasCustomBorder
? ""
: isSelected
? "ring-opacity-50 ring-2 ring-blue-500"
: "hover:ring-opacity-50 hover:ring-1 hover:ring-gray-300";
// 사용자 테두리가 있을 때 선택 상태 표시를 위한 스타일
const selectionStyle =
hasCustomBorder && isSelected
? {
boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.5)", // 외부 그림자로 선택 표시
...style,
}
: style;
return (
<div
className={`absolute cursor-move transition-all ${
isSelected ? "ring-opacity-50 ring-2 ring-blue-500" : "hover:ring-opacity-50 hover:ring-1 hover:ring-gray-300"
}`}
className={`absolute cursor-move transition-all ${defaultRingClass}`}
style={{
left: `${component.position.x}px`,
top: `${component.position.y}px`,
width: `${size.width}px`, // 격자 기반 계산 제거
height: `${size.height}px`,
...style,
...selectionStyle,
}}
onClick={(e) => {
e.stopPropagation();

View File

@@ -171,6 +171,46 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
current: { x: 0, y: 0 },
});
// 선택된 컴포넌트를 항상 레이아웃 최신 값으로 참조 (좌표 실시간 반영용)
const selectedFromLayout = useMemo(() => {
if (!selectedComponent) return null;
return layout.components.find((c) => c.id === selectedComponent.id) || null;
}, [selectedComponent, layout.components]);
// 드래그 중에는 라이브 좌표를 계산하여 속성 패널에 표시
const liveSelectedPosition = useMemo(() => {
if (!selectedFromLayout) return { x: 0, y: 0 };
let x = selectedFromLayout.position.x;
let y = selectedFromLayout.position.y;
if (dragState.isDragging) {
const isSelectedInMulti = groupState.selectedComponents.includes(selectedFromLayout.id);
if (dragState.isMultiDrag && isSelectedInMulti) {
const deltaX = dragState.currentPosition.x - dragState.initialMouse.x;
const deltaY = dragState.currentPosition.y - dragState.initialMouse.y;
x = selectedFromLayout.position.x + deltaX;
y = selectedFromLayout.position.y + deltaY;
} else if (dragState.draggedComponent?.id === selectedFromLayout.id) {
x = dragState.currentPosition.x - dragState.grabOffset.x;
y = dragState.currentPosition.y - dragState.grabOffset.y;
}
}
return { x: Math.round(x), y: Math.round(y) };
}, [
selectedFromLayout,
dragState.isDragging,
dragState.isMultiDrag,
dragState.currentPosition.x,
dragState.currentPosition.y,
dragState.initialMouse.x,
dragState.initialMouse.y,
dragState.grabOffset.x,
dragState.grabOffset.y,
groupState.selectedComponents,
]);
// 컴포넌트의 절대 좌표 계산 (그룹 자식은 부모 오프셋을 누적)
const getAbsolutePosition = useCallback(
(comp: ComponentData) => {
@@ -609,8 +649,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
};
setLayout(newLayout);
saveToHistory(newLayout);
// 선택된 컴포넌트인 경우 즉시 상태도 동기화하여 입력 즉시 반영되도록 처리
if (selectedComponent && selectedComponent.id === componentId) {
const updated = newLayout.components.find((c) => c.id === componentId) || null;
if (updated) setSelectedComponent(updated);
}
},
[layout, saveToHistory],
[layout, saveToHistory, selectedComponent],
);
// 그룹 생성 함수
@@ -1365,10 +1410,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
id="positionX"
type="number"
min="0"
value={selectedComponent.position.x}
onChange={(e) =>
updateComponentProperty(selectedComponent.id, "position.x", parseInt(e.target.value))
}
value={liveSelectedPosition.x}
onChange={(e) => {
const val = (e.target as HTMLInputElement).valueAsNumber;
if (Number.isFinite(val)) {
updateComponentProperty(selectedComponent.id, "position.x", Math.round(val));
}
}}
/>
</div>
<div className="space-y-2">
@@ -1377,10 +1425,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
id="positionY"
type="number"
min="0"
value={selectedComponent.position.y}
onChange={(e) =>
updateComponentProperty(selectedComponent.id, "position.y", parseInt(e.target.value))
}
value={liveSelectedPosition.y}
onChange={(e) => {
const val = (e.target as HTMLInputElement).valueAsNumber;
if (Number.isFinite(val)) {
updateComponentProperty(selectedComponent.id, "position.y", Math.round(val));
}
}}
/>
</div>
</div>
@@ -1392,12 +1443,18 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
<Input
id="width"
type="number"
min="1"
max="12"
min="20"
value={selectedComponent.size.width}
onChange={(e) =>
updateComponentProperty(selectedComponent.id, "size.width", parseInt(e.target.value))
}
onChange={(e) => {
const val = (e.target as HTMLInputElement).valueAsNumber;
if (Number.isFinite(val)) {
updateComponentProperty(
selectedComponent.id,
"size.width",
Math.max(20, Math.round(val)),
);
}
}}
/>
</div>
<div className="space-y-2">
@@ -1407,9 +1464,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
type="number"
min="20"
value={selectedComponent.size.height}
onChange={(e) =>
updateComponentProperty(selectedComponent.id, "size.height", parseInt(e.target.value))
}
onChange={(e) => {
const val = (e.target as HTMLInputElement).valueAsNumber;
if (Number.isFinite(val)) {
updateComponentProperty(
selectedComponent.id,
"size.height",
Math.max(20, Math.round(val)),
);
}
}}
/>
</div>
</div>

View File

@@ -88,29 +88,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
{/* 레이아웃 탭 */}
<TabsContent value="layout" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="width"></Label>
<Input
id="width"
type="text"
placeholder="100px, 50%, auto"
value={localStyle.width || ""}
onChange={(e) => handleStyleChange("width", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="height"></Label>
<Input
id="height"
type="text"
placeholder="100px, 50%, auto"
value={localStyle.height || ""}
onChange={(e) => handleStyleChange("height", e.target.value)}
/>
</div>
</div>
{/* 너비/높이는 위젯 속성에서만 관리하도록 제거 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="display"> </Label>