Compare commits
No commits in common. "5f026e88ababf00762008987ce3bd405215add61" and "e1a5befdf72c5c0bfadb0267cf7564748a124313" have entirely different histories.
5f026e88ab
...
e1a5befdf7
|
|
@ -17,15 +17,6 @@ export async function searchEntity(req: Request, res: Response) {
|
||||||
limit = "20",
|
limit = "20",
|
||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
// tableName 유효성 검증
|
|
||||||
if (!tableName || tableName === "undefined" || tableName === "null") {
|
|
||||||
logger.warn("엔티티 검색 실패: 테이블명이 없음", { tableName });
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: "테이블명이 지정되지 않았습니다. 컴포넌트 설정에서 sourceTable을 확인해주세요.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 멀티테넌시
|
// 멀티테넌시
|
||||||
const companyCode = req.user!.companyCode;
|
const companyCode = req.user!.companyCode;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import { ComponentData } from "@/types/screen";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
|
||||||
|
|
||||||
interface ScreenModalState {
|
interface ScreenModalState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -56,11 +55,11 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
|
|
||||||
// 폼 데이터 상태 추가
|
// 폼 데이터 상태 추가
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
// 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록)
|
// 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록)
|
||||||
const continuousModeRef = useRef(false);
|
const continuousModeRef = useRef(false);
|
||||||
const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음)
|
const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음)
|
||||||
|
|
||||||
// localStorage에서 연속 모드 상태 복원
|
// localStorage에서 연속 모드 상태 복원
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedMode = localStorage.getItem("screenModal_continuousMode");
|
const savedMode = localStorage.getItem("screenModal_continuousMode");
|
||||||
|
|
@ -122,7 +121,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOpenModal = (event: CustomEvent) => {
|
const handleOpenModal = (event: CustomEvent) => {
|
||||||
const { screenId, title, description, size, urlParams } = event.detail;
|
const { screenId, title, description, size, urlParams } = event.detail;
|
||||||
|
|
||||||
// 🆕 URL 파라미터가 있으면 현재 URL에 추가
|
// 🆕 URL 파라미터가 있으면 현재 URL에 추가
|
||||||
if (urlParams && typeof window !== "undefined") {
|
if (urlParams && typeof window !== "undefined") {
|
||||||
const currentUrl = new URL(window.location.href);
|
const currentUrl = new URL(window.location.href);
|
||||||
|
|
@ -133,7 +132,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
window.history.pushState({}, "", currentUrl.toString());
|
window.history.pushState({}, "", currentUrl.toString());
|
||||||
console.log("✅ URL 파라미터 추가:", urlParams);
|
console.log("✅ URL 파라미터 추가:", urlParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalState({
|
setModalState({
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
screenId,
|
screenId,
|
||||||
|
|
@ -152,7 +151,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
window.history.pushState({}, "", currentUrl.toString());
|
window.history.pushState({}, "", currentUrl.toString());
|
||||||
console.log("🧹 URL 파라미터 제거");
|
console.log("🧹 URL 파라미터 제거");
|
||||||
}
|
}
|
||||||
|
|
||||||
setModalState({
|
setModalState({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
screenId: null,
|
screenId: null,
|
||||||
|
|
@ -173,14 +172,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// console.log("💾 저장 성공 이벤트 수신");
|
// console.log("💾 저장 성공 이벤트 수신");
|
||||||
// console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode);
|
// console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode);
|
||||||
// console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
|
// console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
|
||||||
|
|
||||||
if (isContinuousMode) {
|
if (isContinuousMode) {
|
||||||
// 연속 모드: 폼만 초기화하고 모달은 유지
|
// 연속 모드: 폼만 초기화하고 모달은 유지
|
||||||
// console.log("✅ 연속 모드 활성화 - 폼만 초기화");
|
// console.log("✅ 연속 모드 활성화 - 폼만 초기화");
|
||||||
|
|
||||||
// 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨)
|
// 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨)
|
||||||
setFormData({});
|
setFormData({});
|
||||||
|
|
||||||
toast.success("저장되었습니다. 계속 입력하세요.");
|
toast.success("저장되었습니다. 계속 입력하세요.");
|
||||||
} else {
|
} else {
|
||||||
// 일반 모드: 모달 닫기
|
// 일반 모드: 모달 닫기
|
||||||
|
|
@ -227,7 +226,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
|
|
||||||
// 화면 관리에서 설정한 해상도 사용 (우선순위)
|
// 화면 관리에서 설정한 해상도 사용 (우선순위)
|
||||||
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
|
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
|
||||||
|
|
||||||
let dimensions;
|
let dimensions;
|
||||||
if (screenResolution && screenResolution.width && screenResolution.height) {
|
if (screenResolution && screenResolution.width && screenResolution.height) {
|
||||||
// 화면 관리에서 설정한 해상도 사용
|
// 화면 관리에서 설정한 해상도 사용
|
||||||
|
|
@ -243,7 +242,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
dimensions = calculateScreenDimensions(components);
|
dimensions = calculateScreenDimensions(components);
|
||||||
console.log("⚠️ 자동 계산된 크기 사용:", dimensions);
|
console.log("⚠️ 자동 계산된 크기 사용:", dimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
setScreenDimensions(dimensions);
|
setScreenDimensions(dimensions);
|
||||||
|
|
||||||
setScreenData({
|
setScreenData({
|
||||||
|
|
@ -303,17 +302,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const modalStyle = getModalStyle();
|
const modalStyle = getModalStyle();
|
||||||
|
|
||||||
// 안정적인 modalId를 상태로 저장 (모달이 닫혀도 유지)
|
// 안정적인 modalId를 상태로 저장 (모달이 닫혀도 유지)
|
||||||
const [persistedModalId, setPersistedModalId] = useState<string | undefined>(undefined);
|
const [persistedModalId, setPersistedModalId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
// modalId 생성 및 업데이트
|
// modalId 생성 및 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 모달이 열려있고 screenId가 있을 때만 업데이트
|
// 모달이 열려있고 screenId가 있을 때만 업데이트
|
||||||
if (!modalState.isOpen) return;
|
if (!modalState.isOpen) return;
|
||||||
|
|
||||||
let newModalId: string | undefined;
|
let newModalId: string | undefined;
|
||||||
|
|
||||||
// 1순위: screenId (가장 안정적)
|
// 1순위: screenId (가장 안정적)
|
||||||
if (modalState.screenId) {
|
if (modalState.screenId) {
|
||||||
newModalId = `screen-modal-${modalState.screenId}`;
|
newModalId = `screen-modal-${modalState.screenId}`;
|
||||||
|
|
@ -351,17 +350,11 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// result: newModalId,
|
// result: newModalId,
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newModalId) {
|
if (newModalId) {
|
||||||
setPersistedModalId(newModalId);
|
setPersistedModalId(newModalId);
|
||||||
}
|
}
|
||||||
}, [
|
}, [modalState.isOpen, modalState.screenId, modalState.title, screenData?.screenInfo?.tableName, screenData?.screenInfo?.screenName]);
|
||||||
modalState.isOpen,
|
|
||||||
modalState.screenId,
|
|
||||||
modalState.title,
|
|
||||||
screenData?.screenInfo?.tableName,
|
|
||||||
screenData?.screenInfo?.screenName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||||
|
|
@ -404,7 +397,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
) : screenData ? (
|
) : screenData ? (
|
||||||
<TableOptionsProvider>
|
<TableOptionsProvider>
|
||||||
<div
|
<div
|
||||||
className="relative mx-auto bg-white"
|
className="relative bg-white mx-auto"
|
||||||
style={{
|
style={{
|
||||||
width: `${screenDimensions?.width || 800}px`,
|
width: `${screenDimensions?.width || 800}px`,
|
||||||
height: `${screenDimensions?.height || 600}px`,
|
height: `${screenDimensions?.height || 600}px`,
|
||||||
|
|
@ -417,17 +410,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
const offsetY = screenDimensions?.offsetY || 0;
|
const offsetY = screenDimensions?.offsetY || 0;
|
||||||
|
|
||||||
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
|
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
|
||||||
const adjustedComponent =
|
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
|
||||||
offsetX === 0 && offsetY === 0
|
...component,
|
||||||
? component
|
position: {
|
||||||
: {
|
...component.position,
|
||||||
...component,
|
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
||||||
position: {
|
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
||||||
...component.position,
|
},
|
||||||
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
};
|
||||||
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveScreenViewerDynamic
|
<InteractiveScreenViewerDynamic
|
||||||
|
|
@ -482,7 +472,10 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
||||||
// console.log("🔄 연속 모드 변경:", isChecked);
|
// console.log("🔄 연속 모드 변경:", isChecked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
<Label
|
||||||
|
htmlFor="continuous-mode"
|
||||||
|
className="text-sm font-normal cursor-pointer select-none"
|
||||||
|
>
|
||||||
저장 후 계속 입력 (연속 등록 모드)
|
저장 후 계속 입력 (연속 등록 모드)
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -272,15 +272,19 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||||
right: undefined,
|
right: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
// 디버깅: 크기 정보 로그
|
||||||
// if (component.id && isSelected) {
|
if (component.id && isSelected) {
|
||||||
// console.log("📐 RealtimePreview baseStyle:", {
|
console.log("📐 RealtimePreview baseStyle:", {
|
||||||
// componentId: component.id,
|
componentId: component.id,
|
||||||
// componentType: (component as any).componentType || component.type,
|
componentType: (component as any).componentType || component.type,
|
||||||
// sizeWidth: size?.width,
|
sizeWidth: size?.width,
|
||||||
// sizeHeight: size?.height,
|
sizeHeight: size?.height,
|
||||||
// });
|
styleWidth: componentStyle?.width,
|
||||||
// }
|
styleHeight: componentStyle?.height,
|
||||||
|
baseStyleWidth: baseStyle.width,
|
||||||
|
baseStyleHeight: baseStyle.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 🔍 DOM 렌더링 후 실제 크기 측정
|
// 🔍 DOM 렌더링 후 실제 크기 측정
|
||||||
const innerDivRef = React.useRef<HTMLDivElement>(null);
|
const innerDivRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
|
||||||
|
|
@ -883,12 +883,10 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
|
|
||||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||||
const ConfigPanelWrapper = () => {
|
const ConfigPanelWrapper = () => {
|
||||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
const config = currentConfig.config || definition.defaultConfig || {};
|
||||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
|
||||||
|
|
||||||
const handleConfigChange = (newConfig: any) => {
|
const handleConfigChange = (newConfig: any) => {
|
||||||
// componentConfig 전체를 업데이트
|
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -897,7 +895,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
<Settings className="h-4 w-4 text-primary" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { Badge } from "@/components/ui/badge";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
|
@ -267,12 +266,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
const renderComponentConfigPanel = () => {
|
const renderComponentConfigPanel = () => {
|
||||||
if (!selectedComponent) return null;
|
if (!selectedComponent) return null;
|
||||||
|
|
||||||
// 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지
|
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
||||||
const componentType =
|
|
||||||
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
|
|
||||||
selectedComponent.componentConfig?.type ||
|
|
||||||
selectedComponent.componentConfig?.id ||
|
|
||||||
selectedComponent.type;
|
|
||||||
|
|
||||||
const handleUpdateProperty = (path: string, value: any) => {
|
const handleUpdateProperty = (path: string, value: any) => {
|
||||||
onUpdateProperty(selectedComponent.id, path, value);
|
onUpdateProperty(selectedComponent.id, path, value);
|
||||||
|
|
@ -282,15 +276,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
|
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
|
||||||
const componentId =
|
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
|
||||||
selectedComponent.componentType || // ⭐ section-card 등
|
|
||||||
selectedComponent.componentConfig?.type ||
|
|
||||||
selectedComponent.componentConfig?.id;
|
|
||||||
|
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
const definition = ComponentRegistry.getComponent(componentId);
|
const definition = ComponentRegistry.getComponent(componentId);
|
||||||
|
|
||||||
if (definition?.configPanel) {
|
if (definition?.configPanel) {
|
||||||
const ConfigPanelComponent = definition.configPanel;
|
const ConfigPanelComponent = definition.configPanel;
|
||||||
const currentConfig = selectedComponent.componentConfig || {};
|
const currentConfig = selectedComponent.componentConfig || {};
|
||||||
|
|
@ -304,12 +293,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||||
const ConfigPanelWrapper = () => {
|
const ConfigPanelWrapper = () => {
|
||||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
const config = currentConfig.config || definition.defaultConfig || {};
|
||||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
|
||||||
|
|
||||||
const handleConfigChange = (newConfig: any) => {
|
const handleConfigChange = (newConfig: any) => {
|
||||||
// componentConfig 전체를 업데이트
|
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -318,19 +305,18 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
<Settings className="h-4 w-4 text-primary" />
|
<Settings className="h-4 w-4 text-primary" />
|
||||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||||
</div>
|
</div>
|
||||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
||||||
} else {
|
} else {
|
||||||
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
|
console.warn("⚠️ ConfigPanel 없음:", {
|
||||||
componentId,
|
componentId,
|
||||||
definitionName: definition?.name,
|
definitionName: definition?.name,
|
||||||
hasDefinition: !!definition,
|
hasDefinition: !!definition,
|
||||||
});
|
});
|
||||||
// ConfigPanel이 없으면 아래 switch case로 넘어감
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -377,280 +363,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
case "badge-status":
|
case "badge-status":
|
||||||
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||||
|
|
||||||
case "section-card":
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold">Section Card 설정</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
제목과 테두리가 있는 명확한 그룹화 컨테이너
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 헤더 표시 */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="showHeader"
|
|
||||||
checked={selectedComponent.componentConfig?.showHeader !== false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
|
|
||||||
헤더 표시
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 제목 */}
|
|
||||||
{selectedComponent.componentConfig?.showHeader !== false && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">제목</Label>
|
|
||||||
<Input
|
|
||||||
value={selectedComponent.componentConfig?.title || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.title", e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="섹션 제목 입력"
|
|
||||||
className="h-9 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 설명 */}
|
|
||||||
{selectedComponent.componentConfig?.showHeader !== false && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">설명 (선택)</Label>
|
|
||||||
<Textarea
|
|
||||||
value={selectedComponent.componentConfig?.description || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.description", e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="섹션 설명 입력"
|
|
||||||
className="text-xs resize-none"
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 패딩 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">내부 여백</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.padding || "md"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (12px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (24px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (32px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 배경색 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">배경색</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.backgroundColor || "default"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">기본 (카드)</SelectItem>
|
|
||||||
<SelectItem value="muted">회색</SelectItem>
|
|
||||||
<SelectItem value="transparent">투명</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 스타일 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">테두리 스타일</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.borderStyle || "solid"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.borderStyle", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="solid">실선</SelectItem>
|
|
||||||
<SelectItem value="dashed">점선</SelectItem>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 접기/펼치기 기능 */}
|
|
||||||
<div className="space-y-2 pt-2 border-t">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="collapsible"
|
|
||||||
checked={selectedComponent.componentConfig?.collapsible || false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.collapsible", checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
|
|
||||||
접기/펼치기 가능
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedComponent.componentConfig?.collapsible && (
|
|
||||||
<div className="flex items-center space-x-2 ml-6">
|
|
||||||
<Checkbox
|
|
||||||
id="defaultOpen"
|
|
||||||
checked={selectedComponent.componentConfig?.defaultOpen !== false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.defaultOpen", checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
|
|
||||||
기본으로 펼치기
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "section-paper":
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold">Section Paper 설정</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
배경색 기반의 미니멀한 그룹화 컨테이너
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 배경색 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">배경색</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.backgroundColor || "default"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.backgroundColor", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">기본 (연한 회색)</SelectItem>
|
|
||||||
<SelectItem value="muted">회색</SelectItem>
|
|
||||||
<SelectItem value="accent">강조 (연한 파랑)</SelectItem>
|
|
||||||
<SelectItem value="primary">브랜드 컬러</SelectItem>
|
|
||||||
<SelectItem value="custom">커스텀</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 커스텀 색상 */}
|
|
||||||
{selectedComponent.componentConfig?.backgroundColor === "custom" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">커스텀 색상</Label>
|
|
||||||
<Input
|
|
||||||
type="color"
|
|
||||||
value={selectedComponent.componentConfig?.customColor || "#f0f0f0"}
|
|
||||||
onChange={(e) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.customColor", e.target.value);
|
|
||||||
}}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 패딩 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">내부 여백</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.padding || "md"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.padding", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (12px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (16px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (24px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 둥근 모서리 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">둥근 모서리</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.roundedCorners || "md"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.roundedCorners", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (2px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (6px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (8px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 그림자 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">그림자</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedComponent.componentConfig?.shadow || "none"}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.shadow", value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게</SelectItem>
|
|
||||||
<SelectItem value="md">중간</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 표시 */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="showBorder"
|
|
||||||
checked={selectedComponent.componentConfig?.showBorder || false}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
handleUpdateProperty(selectedComponent.id, "componentConfig.showBorder", checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
|
|
||||||
미묘한 테두리 표시
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// ConfigPanel이 없는 경우 경고 표시
|
// ConfigPanel이 없는 경우 경고 표시
|
||||||
return (
|
return (
|
||||||
|
|
@ -922,8 +634,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
||||||
const renderDetailTab = () => {
|
const renderDetailTab = () => {
|
||||||
|
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
|
||||||
|
|
||||||
// 1. DataTable 컴포넌트
|
// 1. DataTable 컴포넌트
|
||||||
if (selectedComponent.type === "datatable") {
|
if (selectedComponent.type === "datatable") {
|
||||||
|
console.log("✅ [renderDetailTab] DataTable 컴포넌트");
|
||||||
return (
|
return (
|
||||||
<DataTableConfigPanel
|
<DataTableConfigPanel
|
||||||
component={selectedComponent as DataTableComponent}
|
component={selectedComponent as DataTableComponent}
|
||||||
|
|
@ -980,6 +695,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
|
|
||||||
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
||||||
if (selectedComponent.type === "component") {
|
if (selectedComponent.type === "component") {
|
||||||
|
console.log("✅ [renderDetailTab] Component 타입");
|
||||||
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
||||||
const webType = selectedComponent.componentConfig?.webType;
|
const webType = selectedComponent.componentConfig?.webType;
|
||||||
|
|
||||||
|
|
@ -1039,6 +755,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||||
tables={tables}
|
tables={tables}
|
||||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||||
onChange={(newConfig) => {
|
onChange={(newConfig) => {
|
||||||
|
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
||||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||||
Object.entries(newConfig).forEach(([key, value]) => {
|
Object.entries(newConfig).forEach(([key, value]) => {
|
||||||
handleUpdate(`componentConfig.${key}`, value);
|
handleUpdate(`componentConfig.${key}`, value);
|
||||||
|
|
|
||||||
|
|
@ -79,14 +79,14 @@ export const CategoryValueAddDialog: React.FC<
|
||||||
const valueCode = generateCode(valueLabel);
|
const valueCode = generateCode(valueLabel);
|
||||||
|
|
||||||
onAdd({
|
onAdd({
|
||||||
tableName: "", // CategoryValueManager에서 오버라이드됨
|
tableName: "",
|
||||||
columnName: "", // CategoryValueManager에서 오버라이드됨
|
columnName: "",
|
||||||
valueCode,
|
valueCode,
|
||||||
valueLabel: valueLabel.trim(),
|
valueLabel: valueLabel.trim(),
|
||||||
description: description.trim() || undefined, // 빈 문자열 대신 undefined
|
description: description.trim(),
|
||||||
color: color === "none" ? undefined : color, // "none"은 undefined로
|
color: color,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
} as TableCategoryValue);
|
});
|
||||||
|
|
||||||
// 초기화
|
// 초기화
|
||||||
setValueLabel("");
|
setValueLabel("");
|
||||||
|
|
|
||||||
|
|
@ -308,8 +308,6 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
style: finalStyle, // size를 포함한 최종 style
|
style: finalStyle, // size를 포함한 최종 style
|
||||||
config: component.componentConfig,
|
config: component.componentConfig,
|
||||||
componentConfig: component.componentConfig,
|
componentConfig: component.componentConfig,
|
||||||
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
|
|
||||||
...(component.componentConfig || {}),
|
|
||||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||||
// 새로운 기능들 전달
|
// 새로운 기능들 전달
|
||||||
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
||||||
|
|
|
||||||
|
|
@ -155,16 +155,14 @@ export function EntitySearchModal({
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
results.map((item, index) => {
|
results.map((item, index) => (
|
||||||
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
|
|
||||||
return (
|
|
||||||
<tr
|
<tr
|
||||||
key={uniqueKey}
|
key={item[valueField] || index}
|
||||||
className="border-t hover:bg-accent cursor-pointer transition-colors"
|
className="border-t hover:bg-accent cursor-pointer transition-colors"
|
||||||
onClick={() => handleSelect(item)}
|
onClick={() => handleSelect(item)}
|
||||||
>
|
>
|
||||||
{displayColumns.map((col) => (
|
{displayColumns.map((col, colIndex) => (
|
||||||
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
|
<td key={`${item[valueField] || index}-${col}-${colIndex}`} className="px-4 py-2">
|
||||||
{item[col] || "-"}
|
{item[col] || "-"}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
@ -182,8 +180,7 @@ export function EntitySearchModal({
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
))
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,6 @@ export function useEntitySearch({
|
||||||
|
|
||||||
const search = useCallback(
|
const search = useCallback(
|
||||||
async (text: string, page: number = 1) => {
|
async (text: string, page: number = 1) => {
|
||||||
// tableName 유효성 검증
|
|
||||||
if (!tableName || tableName === "undefined" || tableName === "null") {
|
|
||||||
console.warn("엔티티 검색 건너뜀: tableName이 없음", { tableName });
|
|
||||||
setError("테이블명이 설정되지 않았습니다. 컴포넌트 설정을 확인해주세요.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -67,8 +60,7 @@ export function useEntitySearch({
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Entity search error:", err);
|
console.error("Entity search error:", err);
|
||||||
const errorMessage = err.response?.data?.message || "검색 중 오류가 발생했습니다";
|
setError(err.response?.data?.message || "검색 중 오류가 발생했습니다");
|
||||||
setError(errorMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,6 @@ import "./order-registration-modal/OrderRegistrationModalRenderer";
|
||||||
import "./conditional-container/ConditionalContainerRenderer";
|
import "./conditional-container/ConditionalContainerRenderer";
|
||||||
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
||||||
|
|
||||||
// 🆕 섹션 그룹화 레이아웃 컴포넌트
|
|
||||||
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
|
|
||||||
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 컴포넌트 초기화 함수
|
* 컴포넌트 초기화 함수
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,178 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export interface SectionCardProps {
|
|
||||||
component?: {
|
|
||||||
id: string;
|
|
||||||
componentConfig?: {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
showHeader?: boolean;
|
|
||||||
headerPosition?: "top" | "left";
|
|
||||||
padding?: "none" | "sm" | "md" | "lg";
|
|
||||||
backgroundColor?: "default" | "muted" | "transparent";
|
|
||||||
borderStyle?: "solid" | "dashed" | "none";
|
|
||||||
collapsible?: boolean;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
};
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
onClick?: (e?: React.MouseEvent) => void;
|
|
||||||
isSelected?: boolean;
|
|
||||||
isDesignMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Card 컴포넌트
|
|
||||||
* 제목과 테두리가 있는 명확한 그룹화 컨테이너
|
|
||||||
*/
|
|
||||||
export function SectionCardComponent({
|
|
||||||
component,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
isSelected = false,
|
|
||||||
isDesignMode = false,
|
|
||||||
}: SectionCardProps) {
|
|
||||||
const config = component?.componentConfig || {};
|
|
||||||
const [isOpen, setIsOpen] = React.useState(config.defaultOpen !== false);
|
|
||||||
|
|
||||||
// 🔄 실시간 업데이트를 위해 config에서 직접 읽기
|
|
||||||
const title = config.title || "";
|
|
||||||
const description = config.description || "";
|
|
||||||
const showHeader = config.showHeader !== false; // 기본값: true
|
|
||||||
const padding = config.padding || "md";
|
|
||||||
const backgroundColor = config.backgroundColor || "default";
|
|
||||||
const borderStyle = config.borderStyle || "solid";
|
|
||||||
const collapsible = config.collapsible || false;
|
|
||||||
|
|
||||||
// 🎯 디버깅: config 값 확인
|
|
||||||
React.useEffect(() => {
|
|
||||||
console.log("✅ Section Card Config:", {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
showHeader,
|
|
||||||
fullConfig: config,
|
|
||||||
});
|
|
||||||
}, [config.title, config.description, config.showHeader]);
|
|
||||||
|
|
||||||
// 패딩 매핑
|
|
||||||
const paddingMap = {
|
|
||||||
none: "p-0",
|
|
||||||
sm: "p-3",
|
|
||||||
md: "p-6",
|
|
||||||
lg: "p-8",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 배경색 매핑
|
|
||||||
const backgroundColorMap = {
|
|
||||||
default: "bg-card",
|
|
||||||
muted: "bg-muted/30",
|
|
||||||
transparent: "bg-transparent",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 테두리 스타일 매핑
|
|
||||||
const borderStyleMap = {
|
|
||||||
solid: "border-solid",
|
|
||||||
dashed: "border-dashed",
|
|
||||||
none: "border-none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
if (collapsible) {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"transition-all",
|
|
||||||
backgroundColorMap[backgroundColor],
|
|
||||||
borderStyleMap[borderStyle],
|
|
||||||
borderStyle === "none" && "shadow-none",
|
|
||||||
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2",
|
|
||||||
isDesignMode && !children && "min-h-[150px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={component?.style}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{/* 헤더 */}
|
|
||||||
{showHeader && (title || description || isDesignMode) && (
|
|
||||||
<CardHeader
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer",
|
|
||||||
collapsible && "hover:bg-accent/50 transition-colors"
|
|
||||||
)}
|
|
||||||
onClick={handleToggle}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
{(title || isDesignMode) && (
|
|
||||||
<CardTitle className="text-xl font-semibold">
|
|
||||||
{title || (isDesignMode ? "섹션 제목" : "")}
|
|
||||||
</CardTitle>
|
|
||||||
)}
|
|
||||||
{(description || isDesignMode) && (
|
|
||||||
<CardDescription className="text-sm text-muted-foreground mt-1.5">
|
|
||||||
{description || (isDesignMode ? "섹션 설명 (선택사항)" : "")}
|
|
||||||
</CardDescription>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{collapsible && (
|
|
||||||
<div className={cn(
|
|
||||||
"ml-4 transition-transform",
|
|
||||||
isOpen ? "rotate-180" : "rotate-0"
|
|
||||||
)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="m6 9 6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 컨텐츠 */}
|
|
||||||
{(!collapsible || isOpen) && (
|
|
||||||
<CardContent className={cn(paddingMap[padding])}>
|
|
||||||
{/* 디자인 모드에서 빈 상태 안내 */}
|
|
||||||
{isDesignMode && !children && (
|
|
||||||
<div className="flex items-center justify-center py-12 text-muted-foreground text-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="mb-2">🃏 Section Card</div>
|
|
||||||
<div className="text-xs">컴포넌트를 이곳에 배치하세요</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 자식 컴포넌트들 */}
|
|
||||||
{children}
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
|
|
||||||
interface SectionCardConfigPanelProps {
|
|
||||||
config: any;
|
|
||||||
onChange: (config: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SectionCardConfigPanel({
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}: SectionCardConfigPanelProps) {
|
|
||||||
const handleChange = (key: string, value: any) => {
|
|
||||||
const newConfig = {
|
|
||||||
...config,
|
|
||||||
[key]: value,
|
|
||||||
};
|
|
||||||
onChange(newConfig);
|
|
||||||
|
|
||||||
// 🎯 실시간 업데이트를 위한 이벤트 발생
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.dispatchEvent(new CustomEvent("componentConfigChanged", {
|
|
||||||
detail: { config: newConfig }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold">Section Card 설정</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
제목과 테두리가 있는 명확한 그룹화 컨테이너
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 헤더 표시 */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="showHeader"
|
|
||||||
checked={config.showHeader !== false}
|
|
||||||
onCheckedChange={(checked) => handleChange("showHeader", checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
|
|
||||||
헤더 표시
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 제목 */}
|
|
||||||
{config.showHeader !== false && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">제목</Label>
|
|
||||||
<Input
|
|
||||||
value={config.title || ""}
|
|
||||||
onChange={(e) => handleChange("title", e.target.value)}
|
|
||||||
placeholder="섹션 제목 입력"
|
|
||||||
className="h-9 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 설명 */}
|
|
||||||
{config.showHeader !== false && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">설명 (선택)</Label>
|
|
||||||
<Textarea
|
|
||||||
value={config.description || ""}
|
|
||||||
onChange={(e) => handleChange("description", e.target.value)}
|
|
||||||
placeholder="섹션 설명 입력"
|
|
||||||
className="text-xs resize-none"
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 패딩 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">내부 여백</Label>
|
|
||||||
<Select
|
|
||||||
value={config.padding || "md"}
|
|
||||||
onValueChange={(value) => handleChange("padding", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (12px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (24px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (32px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 배경색 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">배경색</Label>
|
|
||||||
<Select
|
|
||||||
value={config.backgroundColor || "default"}
|
|
||||||
onValueChange={(value) => handleChange("backgroundColor", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">기본 (카드)</SelectItem>
|
|
||||||
<SelectItem value="muted">회색</SelectItem>
|
|
||||||
<SelectItem value="transparent">투명</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 스타일 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">테두리 스타일</Label>
|
|
||||||
<Select
|
|
||||||
value={config.borderStyle || "solid"}
|
|
||||||
onValueChange={(value) => handleChange("borderStyle", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="solid">실선</SelectItem>
|
|
||||||
<SelectItem value="dashed">점선</SelectItem>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 접기/펼치기 기능 */}
|
|
||||||
<div className="space-y-2 pt-2 border-t">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="collapsible"
|
|
||||||
checked={config.collapsible || false}
|
|
||||||
onCheckedChange={(checked) => handleChange("collapsible", checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
|
|
||||||
접기/펼치기 가능
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{config.collapsible && (
|
|
||||||
<div className="flex items-center space-x-2 ml-6">
|
|
||||||
<Checkbox
|
|
||||||
id="defaultOpen"
|
|
||||||
checked={config.defaultOpen !== false}
|
|
||||||
onCheckedChange={(checked) => handleChange("defaultOpen", checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
|
|
||||||
기본으로 펼치기
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
||||||
import { SectionCardDefinition } from "./index";
|
|
||||||
import { SectionCardComponent } from "./SectionCardComponent";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Card 렌더러
|
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
|
||||||
*/
|
|
||||||
export class SectionCardRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = SectionCardDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
|
||||||
return <SectionCardComponent {...this.props} renderer={this} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
SectionCardRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
SectionCardRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
||||||
import { ComponentCategory } from "@/types/component";
|
|
||||||
import { SectionCardComponent } from "./SectionCardComponent";
|
|
||||||
import { SectionCardConfigPanel } from "./SectionCardConfigPanel";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Card 컴포넌트 정의
|
|
||||||
* 제목과 테두리가 있는 명확한 그룹화 컨테이너
|
|
||||||
*/
|
|
||||||
export const SectionCardDefinition = createComponentDefinition({
|
|
||||||
id: "section-card",
|
|
||||||
name: "Section Card",
|
|
||||||
nameEng: "Section Card",
|
|
||||||
description: "제목과 테두리가 있는 명확한 그룹화 컨테이너",
|
|
||||||
category: ComponentCategory.LAYOUT,
|
|
||||||
webType: "custom",
|
|
||||||
component: SectionCardComponent,
|
|
||||||
defaultConfig: {
|
|
||||||
title: "섹션 제목",
|
|
||||||
description: "",
|
|
||||||
showHeader: true,
|
|
||||||
padding: "md",
|
|
||||||
backgroundColor: "default",
|
|
||||||
borderStyle: "solid",
|
|
||||||
collapsible: false,
|
|
||||||
defaultOpen: true,
|
|
||||||
},
|
|
||||||
defaultSize: { width: 800, height: 250 },
|
|
||||||
configPanel: SectionCardConfigPanel,
|
|
||||||
icon: "LayoutPanelTop",
|
|
||||||
tags: ["섹션", "그룹", "카드", "컨테이너", "제목", "card"],
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "WACE",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 컴포넌트는 SectionCardRenderer에서 자동 등록됩니다
|
|
||||||
|
|
||||||
export { SectionCardComponent } from "./SectionCardComponent";
|
|
||||||
export { SectionCardConfigPanel } from "./SectionCardConfigPanel";
|
|
||||||
export { SectionCardRenderer } from "./SectionCardRenderer";
|
|
||||||
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
export interface SectionPaperProps {
|
|
||||||
component?: {
|
|
||||||
id: string;
|
|
||||||
componentConfig?: {
|
|
||||||
backgroundColor?: "default" | "muted" | "accent" | "primary" | "custom";
|
|
||||||
customColor?: string;
|
|
||||||
showBorder?: boolean;
|
|
||||||
borderStyle?: "none" | "subtle";
|
|
||||||
padding?: "none" | "sm" | "md" | "lg";
|
|
||||||
roundedCorners?: "none" | "sm" | "md" | "lg";
|
|
||||||
shadow?: "none" | "sm" | "md";
|
|
||||||
};
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
onClick?: (e?: React.MouseEvent) => void;
|
|
||||||
isSelected?: boolean;
|
|
||||||
isDesignMode?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Paper 컴포넌트
|
|
||||||
* 배경색만 있는 미니멀한 그룹화 컨테이너 (색종이 컨셉)
|
|
||||||
*/
|
|
||||||
export function SectionPaperComponent({
|
|
||||||
component,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
isSelected = false,
|
|
||||||
isDesignMode = false,
|
|
||||||
}: SectionPaperProps) {
|
|
||||||
const config = component?.componentConfig || {};
|
|
||||||
|
|
||||||
// 배경색 매핑
|
|
||||||
const backgroundColorMap = {
|
|
||||||
default: "bg-muted/20",
|
|
||||||
muted: "bg-muted/30",
|
|
||||||
accent: "bg-accent/20",
|
|
||||||
primary: "bg-primary/5",
|
|
||||||
custom: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 패딩 매핑
|
|
||||||
const paddingMap = {
|
|
||||||
none: "p-0",
|
|
||||||
sm: "p-3",
|
|
||||||
md: "p-4",
|
|
||||||
lg: "p-6",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 둥근 모서리 매핑
|
|
||||||
const roundedMap = {
|
|
||||||
none: "rounded-none",
|
|
||||||
sm: "rounded-sm",
|
|
||||||
md: "rounded-md",
|
|
||||||
lg: "rounded-lg",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 그림자 매핑
|
|
||||||
const shadowMap = {
|
|
||||||
none: "",
|
|
||||||
sm: "shadow-sm",
|
|
||||||
md: "shadow-md",
|
|
||||||
};
|
|
||||||
|
|
||||||
const backgroundColor = config.backgroundColor || "default";
|
|
||||||
const padding = config.padding || "md";
|
|
||||||
const rounded = config.roundedCorners || "md";
|
|
||||||
const shadow = config.shadow || "none";
|
|
||||||
const showBorder = config.showBorder || false;
|
|
||||||
const borderStyle = config.borderStyle || "subtle";
|
|
||||||
|
|
||||||
// 커스텀 배경색 처리
|
|
||||||
const customBgStyle =
|
|
||||||
backgroundColor === "custom" && config.customColor
|
|
||||||
? { backgroundColor: config.customColor }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
// 기본 스타일
|
|
||||||
"relative transition-colors",
|
|
||||||
|
|
||||||
// 배경색
|
|
||||||
backgroundColor !== "custom" && backgroundColorMap[backgroundColor],
|
|
||||||
|
|
||||||
// 패딩
|
|
||||||
paddingMap[padding],
|
|
||||||
|
|
||||||
// 둥근 모서리
|
|
||||||
roundedMap[rounded],
|
|
||||||
|
|
||||||
// 그림자
|
|
||||||
shadowMap[shadow],
|
|
||||||
|
|
||||||
// 테두리 (선택)
|
|
||||||
showBorder &&
|
|
||||||
borderStyle === "subtle" &&
|
|
||||||
"border border-border/30",
|
|
||||||
|
|
||||||
// 디자인 모드에서 선택된 상태
|
|
||||||
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2",
|
|
||||||
|
|
||||||
// 디자인 모드에서 빈 상태 표시
|
|
||||||
isDesignMode && !children && "min-h-[100px] border-2 border-dashed border-muted-foreground/30",
|
|
||||||
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
...customBgStyle,
|
|
||||||
...component?.style,
|
|
||||||
}}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{/* 디자인 모드에서 빈 상태 안내 */}
|
|
||||||
{isDesignMode && !children && (
|
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="mb-1">📄 Section Paper</div>
|
|
||||||
<div className="text-xs">컴포넌트를 이곳에 배치하세요</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 자식 컴포넌트들 */}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
|
|
||||||
interface SectionPaperConfigPanelProps {
|
|
||||||
config: any;
|
|
||||||
onChange: (config: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SectionPaperConfigPanel({
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}: SectionPaperConfigPanelProps) {
|
|
||||||
const handleChange = (key: string, value: any) => {
|
|
||||||
const newConfig = {
|
|
||||||
...config,
|
|
||||||
[key]: value,
|
|
||||||
};
|
|
||||||
onChange(newConfig);
|
|
||||||
|
|
||||||
// 🎯 실시간 업데이트를 위한 이벤트 발생
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.dispatchEvent(new CustomEvent("componentConfigChanged", {
|
|
||||||
detail: { config: newConfig }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="text-sm font-semibold">Section Paper 설정</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
배경색 기반의 미니멀한 그룹화 컨테이너
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 배경색 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">배경색</Label>
|
|
||||||
<Select
|
|
||||||
value={config.backgroundColor || "default"}
|
|
||||||
onValueChange={(value) => handleChange("backgroundColor", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="default">기본 (연한 회색)</SelectItem>
|
|
||||||
<SelectItem value="muted">회색</SelectItem>
|
|
||||||
<SelectItem value="accent">강조 (연한 파랑)</SelectItem>
|
|
||||||
<SelectItem value="primary">브랜드 컬러</SelectItem>
|
|
||||||
<SelectItem value="custom">커스텀</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 커스텀 색상 */}
|
|
||||||
{config.backgroundColor === "custom" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">커스텀 색상</Label>
|
|
||||||
<Input
|
|
||||||
type="color"
|
|
||||||
value={config.customColor || "#f0f0f0"}
|
|
||||||
onChange={(e) => handleChange("customColor", e.target.value)}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 패딩 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">내부 여백</Label>
|
|
||||||
<Select
|
|
||||||
value={config.padding || "md"}
|
|
||||||
onValueChange={(value) => handleChange("padding", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (12px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (16px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (24px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 둥근 모서리 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">둥근 모서리</Label>
|
|
||||||
<Select
|
|
||||||
value={config.roundedCorners || "md"}
|
|
||||||
onValueChange={(value) => handleChange("roundedCorners", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게 (2px)</SelectItem>
|
|
||||||
<SelectItem value="md">중간 (6px)</SelectItem>
|
|
||||||
<SelectItem value="lg">크게 (8px)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 그림자 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">그림자</Label>
|
|
||||||
<Select
|
|
||||||
value={config.shadow || "none"}
|
|
||||||
onValueChange={(value) => handleChange("shadow", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-9 text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">없음</SelectItem>
|
|
||||||
<SelectItem value="sm">작게</SelectItem>
|
|
||||||
<SelectItem value="md">중간</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 테두리 표시 */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="showBorder"
|
|
||||||
checked={config.showBorder || false}
|
|
||||||
onCheckedChange={(checked) => handleChange("showBorder", checked)}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
|
|
||||||
미묘한 테두리 표시
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
||||||
import { SectionPaperDefinition } from "./index";
|
|
||||||
import { SectionPaperComponent } from "./SectionPaperComponent";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Paper 렌더러
|
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
|
||||||
*/
|
|
||||||
export class SectionPaperRenderer extends AutoRegisteringComponentRenderer {
|
|
||||||
static componentDefinition = SectionPaperDefinition;
|
|
||||||
|
|
||||||
render(): React.ReactElement {
|
|
||||||
return <SectionPaperComponent {...this.props} renderer={this} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 자동 등록 실행
|
|
||||||
SectionPaperRenderer.registerSelf();
|
|
||||||
|
|
||||||
// Hot Reload 지원 (개발 모드)
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
SectionPaperRenderer.enableHotReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
|
||||||
import { ComponentCategory } from "@/types/component";
|
|
||||||
import { SectionPaperComponent } from "./SectionPaperComponent";
|
|
||||||
import { SectionPaperConfigPanel } from "./SectionPaperConfigPanel";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section Paper 컴포넌트 정의
|
|
||||||
* 배경색 기반의 미니멀한 그룹화 컨테이너 (색종이 컨셉)
|
|
||||||
*/
|
|
||||||
export const SectionPaperDefinition = createComponentDefinition({
|
|
||||||
id: "section-paper",
|
|
||||||
name: "Section Paper",
|
|
||||||
nameEng: "Section Paper",
|
|
||||||
description: "배경색 기반의 미니멀한 그룹화 컨테이너 (색종이 컨셉)",
|
|
||||||
category: ComponentCategory.LAYOUT,
|
|
||||||
webType: "custom",
|
|
||||||
component: SectionPaperComponent,
|
|
||||||
defaultConfig: {
|
|
||||||
backgroundColor: "default",
|
|
||||||
padding: "md",
|
|
||||||
roundedCorners: "md",
|
|
||||||
shadow: "none",
|
|
||||||
showBorder: false,
|
|
||||||
},
|
|
||||||
defaultSize: { width: 800, height: 200 },
|
|
||||||
configPanel: SectionPaperConfigPanel,
|
|
||||||
icon: "Square",
|
|
||||||
tags: ["섹션", "그룹", "배경", "컨테이너", "색종이", "paper"],
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "WACE",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 컴포넌트는 SectionPaperRenderer에서 자동 등록됩니다
|
|
||||||
|
|
||||||
export { SectionPaperComponent } from "./SectionPaperComponent";
|
|
||||||
export { SectionPaperConfigPanel } from "./SectionPaperConfigPanel";
|
|
||||||
export { SectionPaperRenderer } from "./SectionPaperRenderer";
|
|
||||||
|
|
||||||
|
|
@ -39,9 +39,6 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||||
// 🆕 선택 항목 상세입력
|
// 🆕 선택 항목 상세입력
|
||||||
"selected-items-detail-input": () =>
|
"selected-items-detail-input": () =>
|
||||||
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
|
import("@/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel"),
|
||||||
// 🆕 섹션 그룹화 레이아웃
|
|
||||||
"section-card": () => import("@/lib/registry/components/section-card/SectionCardConfigPanel"),
|
|
||||||
"section-paper": () => import("@/lib/registry/components/section-paper/SectionPaperConfigPanel"),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ConfigPanel 컴포넌트 캐시
|
// ConfigPanel 컴포넌트 캐시
|
||||||
|
|
@ -74,8 +71,6 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
||||||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||||
module.ButtonConfigPanel || // button-primary의 export명
|
module.ButtonConfigPanel || // button-primary의 export명
|
||||||
module.SectionCardConfigPanel || // section-card의 export명
|
|
||||||
module.SectionPaperConfigPanel || // section-paper의 export명
|
|
||||||
module.default;
|
module.default;
|
||||||
|
|
||||||
if (!ConfigPanelComponent) {
|
if (!ConfigPanelComponent) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue