Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
commit
5f026e88ab
|
|
@ -17,6 +17,15 @@ export async function searchEntity(req: Request, res: Response) {
|
|||
limit = "20",
|
||||
} = 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { ComponentData } from "@/types/screen";
|
|||
import { toast } from "sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { TableOptionsProvider } from "@/contexts/TableOptionsContext";
|
||||
import { TableSearchWidgetHeightProvider } from "@/contexts/TableSearchWidgetHeightContext";
|
||||
|
||||
interface ScreenModalState {
|
||||
isOpen: boolean;
|
||||
|
|
@ -55,11 +56,11 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
|
||||
// 폼 데이터 상태 추가
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
|
||||
// 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록)
|
||||
const continuousModeRef = useRef(false);
|
||||
const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음)
|
||||
|
||||
|
||||
// localStorage에서 연속 모드 상태 복원
|
||||
useEffect(() => {
|
||||
const savedMode = localStorage.getItem("screenModal_continuousMode");
|
||||
|
|
@ -121,7 +122,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
useEffect(() => {
|
||||
const handleOpenModal = (event: CustomEvent) => {
|
||||
const { screenId, title, description, size, urlParams } = event.detail;
|
||||
|
||||
|
||||
// 🆕 URL 파라미터가 있으면 현재 URL에 추가
|
||||
if (urlParams && typeof window !== "undefined") {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
|
|
@ -132,7 +133,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
window.history.pushState({}, "", currentUrl.toString());
|
||||
console.log("✅ URL 파라미터 추가:", urlParams);
|
||||
}
|
||||
|
||||
|
||||
setModalState({
|
||||
isOpen: true,
|
||||
screenId,
|
||||
|
|
@ -151,7 +152,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
window.history.pushState({}, "", currentUrl.toString());
|
||||
console.log("🧹 URL 파라미터 제거");
|
||||
}
|
||||
|
||||
|
||||
setModalState({
|
||||
isOpen: false,
|
||||
screenId: null,
|
||||
|
|
@ -172,14 +173,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// console.log("💾 저장 성공 이벤트 수신");
|
||||
// console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode);
|
||||
// console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
|
||||
|
||||
|
||||
if (isContinuousMode) {
|
||||
// 연속 모드: 폼만 초기화하고 모달은 유지
|
||||
// console.log("✅ 연속 모드 활성화 - 폼만 초기화");
|
||||
|
||||
|
||||
// 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨)
|
||||
setFormData({});
|
||||
|
||||
|
||||
toast.success("저장되었습니다. 계속 입력하세요.");
|
||||
} else {
|
||||
// 일반 모드: 모달 닫기
|
||||
|
|
@ -226,7 +227,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
|
||||
// 화면 관리에서 설정한 해상도 사용 (우선순위)
|
||||
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
|
||||
|
||||
|
||||
let dimensions;
|
||||
if (screenResolution && screenResolution.width && screenResolution.height) {
|
||||
// 화면 관리에서 설정한 해상도 사용
|
||||
|
|
@ -242,7 +243,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
dimensions = calculateScreenDimensions(components);
|
||||
console.log("⚠️ 자동 계산된 크기 사용:", dimensions);
|
||||
}
|
||||
|
||||
|
||||
setScreenDimensions(dimensions);
|
||||
|
||||
setScreenData({
|
||||
|
|
@ -302,17 +303,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
};
|
||||
|
||||
const modalStyle = getModalStyle();
|
||||
|
||||
|
||||
// 안정적인 modalId를 상태로 저장 (모달이 닫혀도 유지)
|
||||
const [persistedModalId, setPersistedModalId] = useState<string | undefined>(undefined);
|
||||
|
||||
|
||||
// modalId 생성 및 업데이트
|
||||
useEffect(() => {
|
||||
// 모달이 열려있고 screenId가 있을 때만 업데이트
|
||||
if (!modalState.isOpen) return;
|
||||
|
||||
|
||||
let newModalId: string | undefined;
|
||||
|
||||
|
||||
// 1순위: screenId (가장 안정적)
|
||||
if (modalState.screenId) {
|
||||
newModalId = `screen-modal-${modalState.screenId}`;
|
||||
|
|
@ -350,11 +351,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// result: newModalId,
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
if (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 (
|
||||
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
|
||||
|
|
@ -397,7 +404,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
) : screenData ? (
|
||||
<TableOptionsProvider>
|
||||
<div
|
||||
className="relative bg-white mx-auto"
|
||||
className="relative mx-auto bg-white"
|
||||
style={{
|
||||
width: `${screenDimensions?.width || 800}px`,
|
||||
height: `${screenDimensions?.height || 600}px`,
|
||||
|
|
@ -410,14 +417,17 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
const offsetY = screenDimensions?.offsetY || 0;
|
||||
|
||||
// offset이 0이면 원본 위치 사용 (화면 관리 해상도 사용 시)
|
||||
const adjustedComponent = (offsetX === 0 && offsetY === 0) ? component : {
|
||||
...component,
|
||||
position: {
|
||||
...component.position,
|
||||
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
||||
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
||||
},
|
||||
};
|
||||
const adjustedComponent =
|
||||
offsetX === 0 && offsetY === 0
|
||||
? component
|
||||
: {
|
||||
...component,
|
||||
position: {
|
||||
...component.position,
|
||||
x: parseFloat(component.position?.x?.toString() || "0") - offsetX,
|
||||
y: parseFloat(component.position?.y?.toString() || "0") - offsetY,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<InteractiveScreenViewerDynamic
|
||||
|
|
@ -472,10 +482,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
|
|||
// console.log("🔄 연속 모드 변경:", isChecked);
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="continuous-mode"
|
||||
className="text-sm font-normal cursor-pointer select-none"
|
||||
>
|
||||
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">
|
||||
저장 후 계속 입력 (연속 등록 모드)
|
||||
</Label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -272,19 +272,15 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
|||
right: undefined,
|
||||
};
|
||||
|
||||
// 디버깅: 크기 정보 로그
|
||||
if (component.id && isSelected) {
|
||||
console.log("📐 RealtimePreview baseStyle:", {
|
||||
componentId: component.id,
|
||||
componentType: (component as any).componentType || component.type,
|
||||
sizeWidth: size?.width,
|
||||
sizeHeight: size?.height,
|
||||
styleWidth: componentStyle?.width,
|
||||
styleHeight: componentStyle?.height,
|
||||
baseStyleWidth: baseStyle.width,
|
||||
baseStyleHeight: baseStyle.height,
|
||||
});
|
||||
}
|
||||
// 크기 정보는 필요시에만 디버깅 (개발 중 문제 발생 시 주석 해제)
|
||||
// if (component.id && isSelected) {
|
||||
// console.log("📐 RealtimePreview baseStyle:", {
|
||||
// componentId: component.id,
|
||||
// componentType: (component as any).componentType || component.type,
|
||||
// sizeWidth: size?.width,
|
||||
// sizeHeight: size?.height,
|
||||
// });
|
||||
// }
|
||||
|
||||
// 🔍 DOM 렌더링 후 실제 크기 측정
|
||||
const innerDivRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -883,10 +883,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
|
||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||
const ConfigPanelWrapper = () => {
|
||||
const config = currentConfig.config || definition.defaultConfig || {};
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -895,7 +897,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
<Settings className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||
</div>
|
||||
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
||||
import {
|
||||
|
|
@ -266,7 +267,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
const renderComponentConfigPanel = () => {
|
||||
if (!selectedComponent) return null;
|
||||
|
||||
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
||||
// 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지
|
||||
const componentType =
|
||||
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
|
||||
selectedComponent.componentConfig?.type ||
|
||||
selectedComponent.componentConfig?.id ||
|
||||
selectedComponent.type;
|
||||
|
||||
const handleUpdateProperty = (path: string, value: any) => {
|
||||
onUpdateProperty(selectedComponent.id, path, value);
|
||||
|
|
@ -276,10 +282,15 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||
};
|
||||
|
||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
|
||||
const componentId = selectedComponent.componentConfig?.type || selectedComponent.componentConfig?.id;
|
||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
|
||||
const componentId =
|
||||
selectedComponent.componentType || // ⭐ section-card 등
|
||||
selectedComponent.componentConfig?.type ||
|
||||
selectedComponent.componentConfig?.id;
|
||||
|
||||
if (componentId) {
|
||||
const definition = ComponentRegistry.getComponent(componentId);
|
||||
|
||||
if (definition?.configPanel) {
|
||||
const ConfigPanelComponent = definition.configPanel;
|
||||
const currentConfig = selectedComponent.componentConfig || {};
|
||||
|
|
@ -293,10 +304,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
|
||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||
const ConfigPanelWrapper = () => {
|
||||
const config = currentConfig.config || definition.defaultConfig || {};
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -305,18 +318,19 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
<Settings className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||
</div>
|
||||
<ConfigPanelComponent config={config} onConfigChange={handleConfigChange} />
|
||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
||||
} else {
|
||||
console.warn("⚠️ ConfigPanel 없음:", {
|
||||
console.warn("⚠️ ComponentRegistry에서 ConfigPanel을 찾을 수 없음 - switch case로 이동:", {
|
||||
componentId,
|
||||
definitionName: definition?.name,
|
||||
hasDefinition: !!definition,
|
||||
});
|
||||
// ConfigPanel이 없으면 아래 switch case로 넘어감
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -363,6 +377,280 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
case "badge-status":
|
||||
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:
|
||||
// ConfigPanel이 없는 경우 경고 표시
|
||||
return (
|
||||
|
|
@ -634,11 +922,8 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
|
||||
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
||||
const renderDetailTab = () => {
|
||||
console.log("🔍 [renderDetailTab] selectedComponent.type:", selectedComponent.type);
|
||||
|
||||
// 1. DataTable 컴포넌트
|
||||
if (selectedComponent.type === "datatable") {
|
||||
console.log("✅ [renderDetailTab] DataTable 컴포넌트");
|
||||
return (
|
||||
<DataTableConfigPanel
|
||||
component={selectedComponent as DataTableComponent}
|
||||
|
|
@ -695,7 +980,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
|
||||
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
||||
if (selectedComponent.type === "component") {
|
||||
console.log("✅ [renderDetailTab] Component 타입");
|
||||
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
||||
const webType = selectedComponent.componentConfig?.webType;
|
||||
|
||||
|
|
@ -755,7 +1039,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
|||
tables={tables}
|
||||
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
|
||||
onChange={(newConfig) => {
|
||||
console.log("🔄 DynamicComponentConfigPanel onChange:", newConfig);
|
||||
// 개별 속성별로 업데이트하여 다른 속성과의 충돌 방지
|
||||
Object.entries(newConfig).forEach(([key, value]) => {
|
||||
handleUpdate(`componentConfig.${key}`, value);
|
||||
|
|
|
|||
|
|
@ -79,14 +79,14 @@ export const CategoryValueAddDialog: React.FC<
|
|||
const valueCode = generateCode(valueLabel);
|
||||
|
||||
onAdd({
|
||||
tableName: "",
|
||||
columnName: "",
|
||||
tableName: "", // CategoryValueManager에서 오버라이드됨
|
||||
columnName: "", // CategoryValueManager에서 오버라이드됨
|
||||
valueCode,
|
||||
valueLabel: valueLabel.trim(),
|
||||
description: description.trim(),
|
||||
color: color,
|
||||
description: description.trim() || undefined, // 빈 문자열 대신 undefined
|
||||
color: color === "none" ? undefined : color, // "none"은 undefined로
|
||||
isDefault: false,
|
||||
});
|
||||
} as TableCategoryValue);
|
||||
|
||||
// 초기화
|
||||
setValueLabel("");
|
||||
|
|
|
|||
|
|
@ -308,6 +308,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
style: finalStyle, // size를 포함한 최종 style
|
||||
config: component.componentConfig,
|
||||
componentConfig: component.componentConfig,
|
||||
// componentConfig의 모든 속성을 props로 spread (tableName, displayField 등)
|
||||
...(component.componentConfig || {}),
|
||||
value: currentValue, // formData에서 추출한 현재 값 전달
|
||||
// 새로운 기능들 전달
|
||||
autoGeneration: component.autoGeneration || component.componentConfig?.autoGeneration,
|
||||
|
|
|
|||
|
|
@ -155,14 +155,16 @@ export function EntitySearchModal({
|
|||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
results.map((item, index) => (
|
||||
results.map((item, index) => {
|
||||
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
|
||||
return (
|
||||
<tr
|
||||
key={item[valueField] || index}
|
||||
key={uniqueKey}
|
||||
className="border-t hover:bg-accent cursor-pointer transition-colors"
|
||||
onClick={() => handleSelect(item)}
|
||||
>
|
||||
{displayColumns.map((col, colIndex) => (
|
||||
<td key={`${item[valueField] || index}-${col}-${colIndex}`} className="px-4 py-2">
|
||||
{displayColumns.map((col) => (
|
||||
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
|
||||
{item[col] || "-"}
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -180,7 +182,8 @@ export function EntitySearchModal({
|
|||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ export function useEntitySearch({
|
|||
|
||||
const search = useCallback(
|
||||
async (text: string, page: number = 1) => {
|
||||
// tableName 유효성 검증
|
||||
if (!tableName || tableName === "undefined" || tableName === "null") {
|
||||
console.warn("엔티티 검색 건너뜀: tableName이 없음", { tableName });
|
||||
setError("테이블명이 설정되지 않았습니다. 컴포넌트 설정을 확인해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
|
@ -60,7 +67,8 @@ export function useEntitySearch({
|
|||
}
|
||||
} catch (err: any) {
|
||||
console.error("Entity search error:", err);
|
||||
setError(err.response?.data?.message || "검색 중 오류가 발생했습니다");
|
||||
const errorMessage = err.response?.data?.message || "검색 중 오류가 발생했습니다";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ import "./order-registration-modal/OrderRegistrationModalRenderer";
|
|||
import "./conditional-container/ConditionalContainerRenderer";
|
||||
import "./selected-items-detail-input/SelectedItemsDetailInputRenderer";
|
||||
|
||||
// 🆕 섹션 그룹화 레이아웃 컴포넌트
|
||||
import "./section-paper/SectionPaperRenderer"; // Section Paper (색종이 - 배경색 기반 그룹화) - Renderer 방식
|
||||
import "./section-card/SectionCardRenderer"; // Section Card (제목+테두리 기반 그룹화) - Renderer 방식
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"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();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"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";
|
||||
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"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();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
"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,6 +39,9 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
|||
// 🆕 선택 항목 상세입력
|
||||
"selected-items-detail-input": () =>
|
||||
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 컴포넌트 캐시
|
||||
|
|
@ -71,6 +74,8 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
|||
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
|
||||
module.SelectedItemsDetailInputConfigPanel || // selected-items-detail-input의 export명
|
||||
module.ButtonConfigPanel || // button-primary의 export명
|
||||
module.SectionCardConfigPanel || // section-card의 export명
|
||||
module.SectionPaperConfigPanel || // section-paper의 export명
|
||||
module.default;
|
||||
|
||||
if (!ConfigPanelComponent) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue