Compare commits

..

No commits in common. "5b44a416514eba97364a8f3a29fc4d98f0dcb874" and "8253be00481d2d434271f37d237a540ec00b292f" have entirely different histories.

5 changed files with 29 additions and 246 deletions

View File

@ -730,91 +730,6 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
return activeComps; return activeComps;
}, [formData, conditionalLayers, screenData?.components]); }, [formData, conditionalLayers, screenData?.components]);
// 🆕 이전 활성 레이어 ID 추적 (레이어 전환 감지용)
const prevActiveLayerIdsRef = useRef<string[]>([]);
// 🆕 레이어 전환 시 비활성화된 레이어의 필드값을 formData에서 제거
// (품목우선 → 공급업체우선 전환 시, 품목우선 레이어의 데이터가 남지 않도록)
useEffect(() => {
if (conditionalLayers.length === 0) return;
// 현재 활성 레이어 ID 목록
const currentActiveLayerIds = conditionalLayers
.filter((layer) => {
if (!layer.condition) return false;
const { targetComponentId, operator, value } = layer.condition;
if (!targetComponentId) return false;
const allComponents = screenData?.components || [];
const comp = allComponents.find((c: any) => c.id === targetComponentId);
const fieldKey =
(comp as any)?.overrides?.columnName ||
(comp as any)?.columnName ||
(comp as any)?.componentConfig?.columnName ||
targetComponentId;
const targetValue = formData[fieldKey];
switch (operator) {
case "eq":
return String(targetValue ?? "") === String(value ?? "");
case "neq":
return String(targetValue ?? "") !== String(value ?? "");
case "in":
if (Array.isArray(value)) {
return value.some((v) => String(v) === String(targetValue ?? ""));
} else if (typeof value === "string" && value.includes(",")) {
return value.split(",").map((v) => v.trim()).includes(String(targetValue ?? ""));
}
return false;
default:
return false;
}
})
.map((l) => l.id);
const prevIds = prevActiveLayerIdsRef.current;
// 이전에 활성이었는데 이번에 비활성이 된 레이어 찾기
const deactivatedLayerIds = prevIds.filter((id) => !currentActiveLayerIds.includes(id));
if (deactivatedLayerIds.length > 0) {
// 비활성화된 레이어의 컴포넌트 필드명 수집
const fieldsToRemove: string[] = [];
deactivatedLayerIds.forEach((layerId) => {
const layer = conditionalLayers.find((l) => l.id === layerId);
if (!layer) return;
layer.components.forEach((comp: any) => {
const fieldName =
comp?.overrides?.columnName ||
comp?.columnName ||
comp?.componentConfig?.columnName;
if (fieldName) {
fieldsToRemove.push(fieldName);
}
});
});
if (fieldsToRemove.length > 0) {
console.log("[ScreenModal] 레이어 전환 감지 - 비활성 레이어 필드 제거:", {
deactivatedLayerIds,
fieldsToRemove,
});
setFormData((prev) => {
const cleaned = { ...prev };
fieldsToRemove.forEach((field) => {
delete cleaned[field];
});
return cleaned;
});
}
}
// 현재 상태 저장
prevActiveLayerIdsRef.current = currentActiveLayerIds;
}, [formData, conditionalLayers, screenData?.components]);
// 사용자가 바깥 클릭/ESC/X 버튼으로 닫으려 할 때 // 사용자가 바깥 클릭/ESC/X 버튼으로 닫으려 할 때
// 폼 데이터 변경이 있으면 확인 다이얼로그, 없으면 바로 닫기 // 폼 데이터 변경이 있으면 확인 다이얼로그, 없으면 바로 닫기
const handleCloseAttempt = useCallback(() => { const handleCloseAttempt = useCallback(() => {

View File

@ -233,8 +233,8 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
return; return;
} }
// tableName 확인 (props에서 전달받은 tableName 또는 componentConfig에서 추출) // tableName 확인 (props에서 전달받은 tableName 사용)
const tableNameToUse = tableName || component.componentConfig?.tableName; const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정
if (!tableNameToUse) { if (!tableNameToUse) {
setLoading(false); setLoading(false);

View File

@ -193,7 +193,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [rightData, setRightData] = useState<any[] | any>(null); // 조인 모드는 배열, 상세 모드는 객체 const [rightData, setRightData] = useState<any[] | any>(null); // 조인 모드는 배열, 상세 모드는 객체
const [selectedLeftItem, setSelectedLeftItem] = useState<any>(null); const [selectedLeftItem, setSelectedLeftItem] = useState<any>(null);
const [expandedRightItems, setExpandedRightItems] = useState<Set<string | number>>(new Set()); // 확장된 우측 아이템 const [expandedRightItems, setExpandedRightItems] = useState<Set<string | number>>(new Set()); // 확장된 우측 아이템
const [customLeftSelectedData, setCustomLeftSelectedData] = useState<Record<string, any>>({}); // 커스텀 모드: 좌측 선택 데이터
const [leftSearchQuery, setLeftSearchQuery] = useState(""); const [leftSearchQuery, setLeftSearchQuery] = useState("");
const [rightSearchQuery, setRightSearchQuery] = useState(""); const [rightSearchQuery, setRightSearchQuery] = useState("");
const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingLeft, setIsLoadingLeft] = useState(false);
@ -221,8 +220,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null); const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null);
// 🆕 외부에서 전달받은 선택 상태 사용 (탭 컴포넌트와 동일 구조) // 🆕 외부에서 전달받은 선택 상태 사용 (탭 컴포넌트와 동일 구조)
const selectedPanelComponentId = externalSelectedPanelComponentId || null; const selectedPanelComponentId = externalSelectedPanelComponentId || null;
// 🆕 커스텀 모드: 분할패널 내 탭 컴포넌트의 선택 상태 관리
const [nestedTabSelectedCompId, setNestedTabSelectedCompId] = useState<string | undefined>(undefined);
const rafRef = useRef<number | null>(null); const rafRef = useRef<number | null>(null);
// 🆕 10px 단위 스냅 함수 // 🆕 10px 단위 스냅 함수
@ -302,9 +299,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
rafRef.current = requestAnimationFrame(() => { rafRef.current = requestAnimationFrame(() => {
const deltaX = moveEvent.clientX - startMouseX; const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY; const deltaY = moveEvent.clientY - startMouseY;
// 10px 단위 스냅 적용 const newX = Math.max(0, startLeft + deltaX);
const newX = snapTo10(Math.max(0, startLeft + deltaX)); const newY = Math.max(0, startTop + deltaY);
const newY = snapTo10(Math.max(0, startTop + deltaY));
setDragPosition({ x: newX, y: newY }); setDragPosition({ x: newX, y: newY });
}); });
}; };
@ -320,9 +316,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const deltaX = upEvent.clientX - startMouseX; const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY; const deltaY = upEvent.clientY - startMouseY;
// 10px 단위 스냅 적용 const newX = Math.max(0, startLeft + deltaX);
const newX = snapTo10(Math.max(0, startLeft + deltaX)); const newY = Math.max(0, startTop + deltaY);
const newY = snapTo10(Math.max(0, startTop + deltaY));
setDraggingCompId(null); setDraggingCompId(null);
setDragPosition(null); setDragPosition(null);
@ -332,7 +327,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const panelConfig = componentConfig[panelKey] || {}; const panelConfig = componentConfig[panelKey] || {};
const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) => const updatedComponents = (panelConfig.components || []).map((c: PanelInlineComponent) =>
c.id === comp.id c.id === comp.id
? { ...c, position: { x: newX, y: newY } } ? { ...c, position: { x: Math.round(newX), y: Math.round(newY) } }
: c : c
); );
@ -352,7 +347,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
}, },
[component, componentConfig, onUpdateComponent, snapTo10] [component, componentConfig, onUpdateComponent]
); );
// 🆕 커스텀 모드: 리사이즈 시작 핸들러 // 🆕 커스텀 모드: 리사이즈 시작 핸들러
@ -2605,10 +2600,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// 패널 컴포넌트 선택 시 탭 내 선택 해제
if (comp.componentType !== "v2-tabs-widget") {
setNestedTabSelectedCompId(undefined);
}
onSelectPanelComponent?.("left", comp.id, comp); onSelectPanelComponent?.("left", comp.id, comp);
}} }}
> >
@ -2688,8 +2679,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함 // 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => { onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
console.log("🔍 [SplitPanel-Left] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id }); console.log("🔍 [SplitPanel-Left] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
// 탭 내 컴포넌트 선택 상태 업데이트
setNestedTabSelectedCompId(compId);
// 부모 분할 패널 정보와 함께 전역 이벤트 발생 // 부모 분할 패널 정보와 함께 전역 이벤트 발생
const event = new CustomEvent("nested-tab-component-select", { const event = new CustomEvent("nested-tab-component-select", {
detail: { detail: {
@ -2703,7 +2692,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}); });
window.dispatchEvent(event); window.dispatchEvent(event);
}} }}
selectedTabComponentId={nestedTabSelectedCompId} selectedTabComponentId={undefined}
/> />
</div> </div>
@ -2758,17 +2747,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
component={componentData as any} component={componentData as any}
isDesignMode={false} isDesignMode={false}
formData={{}} formData={{}}
tableName={componentConfig.leftPanel?.tableName}
onFormDataChange={(data: any) => {
// 커스텀 모드: 좌측 카드/테이블 선택 시 데이터 캡처
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
setCustomLeftSelectedData(data.selectedRowsData[0]);
setSelectedLeftItem(data.selectedRowsData[0]);
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
setCustomLeftSelectedData({});
setSelectedLeftItem(null);
}
}}
/> />
</div> </div>
); );
@ -3504,10 +3482,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}} }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
// 패널 컴포넌트 선택 시 탭 내 선택 해제
if (comp.componentType !== "v2-tabs-widget") {
setNestedTabSelectedCompId(undefined);
}
onSelectPanelComponent?.("right", comp.id, comp); onSelectPanelComponent?.("right", comp.id, comp);
}} }}
> >
@ -3587,8 +3561,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함 // 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => { onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
console.log("🔍 [SplitPanel-Right] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id }); console.log("🔍 [SplitPanel-Right] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
// 탭 내 컴포넌트 선택 상태 업데이트
setNestedTabSelectedCompId(compId);
// 부모 분할 패널 정보와 함께 전역 이벤트 발생 // 부모 분할 패널 정보와 함께 전역 이벤트 발생
const event = new CustomEvent("nested-tab-component-select", { const event = new CustomEvent("nested-tab-component-select", {
detail: { detail: {
@ -3602,7 +3574,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}); });
window.dispatchEvent(event); window.dispatchEvent(event);
}} }}
selectedTabComponentId={nestedTabSelectedCompId} selectedTabComponentId={undefined}
/> />
</div> </div>
@ -3645,8 +3617,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<DynamicComponentRenderer <DynamicComponentRenderer
component={componentData as any} component={componentData as any}
isDesignMode={false} isDesignMode={false}
formData={customLeftSelectedData} formData={{}}
tableName={componentConfig.rightPanel?.tableName || componentConfig.leftPanel?.tableName}
/> />
</div> </div>
); );

View File

@ -186,10 +186,9 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
return; return;
} }
// tableColumns prop은 화면의 기본 테이블 컬럼이므로, // 🆕 customTableName이 설정된 경우 반드시 API에서 가져오기
// 다른 테이블을 선택한 경우 반드시 API에서 가져오기 // tableColumns prop은 화면의 기본 테이블 컬럼이므로, customTableName 사용 시 무시
const isUsingDifferentTable = config.selectedTable && screenTableName && config.selectedTable !== screenTableName; const shouldUseTableColumnsProp = !config.useCustomTable && tableColumns && tableColumns.length > 0;
const shouldUseTableColumnsProp = !config.useCustomTable && !isUsingDifferentTable && tableColumns && tableColumns.length > 0;
if (shouldUseTableColumnsProp) { if (shouldUseTableColumnsProp) {
const mappedColumns = tableColumns.map((column: any) => ({ const mappedColumns = tableColumns.map((column: any) => ({
@ -773,113 +772,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
handleChange("columns", columns); handleChange("columns", columns);
}; };
// 테이블 변경 핸들러 - 테이블 변경 시 컬럼 설정 초기화
const handleTableChange = (newTableName: string) => {
if (newTableName === targetTableName) return;
const updatedConfig = {
...config,
selectedTable: newTableName,
// 테이블이 변경되면 컬럼 설정 초기화
columns: [],
};
onChange(updatedConfig);
setTableComboboxOpen(false);
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-sm font-medium"> </div> <div className="text-sm font-medium"> </div>
<div className="space-y-6"> <div className="space-y-6">
{/* 테이블 선택 */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
<p className="text-muted-foreground text-[10px]">
. .
</p>
</div>
<hr className="border-border" />
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
className="h-8 w-full justify-between text-xs"
disabled={loadingTables}
>
<div className="flex items-center gap-2 truncate">
<Table2 className="h-3 w-3 shrink-0" />
<span className="truncate">
{loadingTables
? "테이블 로딩 중..."
: targetTableName
? availableTables.find((t) => t.tableName === targetTableName)?.displayName ||
targetTableName
: "테이블 선택"}
</span>
</div>
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup>
{availableTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.displayName}`}
onSelect={() => handleTableChange(table.tableName)}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
targetTableName === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span>{table.displayName}</span>
{table.displayName !== table.tableName && (
<span className="text-[10px] text-gray-400">{table.tableName}</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{screenTableName && targetTableName !== screenTableName && (
<div className="flex items-center justify-between rounded bg-amber-50 px-2 py-1">
<span className="text-[10px] text-amber-700">
({screenTableName})
</span>
<Button
variant="ghost"
size="sm"
className="h-5 px-1.5 text-[10px] text-amber-700 hover:text-amber-900"
onClick={() => handleTableChange(screenTableName)}
>
</Button>
</div>
)}
</div>
</div>
{/* 툴바 버튼 설정 */} {/* 툴바 버튼 설정 */}
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
@ -1270,11 +1167,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
</div> </div>
)} )}
{!targetTableName ? ( {!screenTableName ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="text-center text-gray-500"> <div className="text-center text-gray-500">
<p> .</p> <p> .</p>
<p className="text-sm"> .</p> <p className="text-sm"> .</p>
</div> </div>
</div> </div>
) : availableColumns.length === 0 ? ( ) : availableColumns.length === 0 ? (

View File

@ -78,9 +78,6 @@ const TabsDesignEditor: React.FC<{
[activeTabId, component, onUpdateComponent, tabs] [activeTabId, component, onUpdateComponent, tabs]
); );
// 10px 단위 스냅 함수
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
// 컴포넌트 드래그 시작 // 컴포넌트 드래그 시작
const handleDragStart = useCallback( const handleDragStart = useCallback(
(e: React.MouseEvent, comp: TabInlineComponent) => { (e: React.MouseEvent, comp: TabInlineComponent) => {
@ -107,9 +104,9 @@ const TabsDesignEditor: React.FC<{
const deltaX = moveEvent.clientX - startMouseX; const deltaX = moveEvent.clientX - startMouseX;
const deltaY = moveEvent.clientY - startMouseY; const deltaY = moveEvent.clientY - startMouseY;
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용) // 새 위치 = 시작 위치 + 이동량
const newX = snapTo10(Math.max(0, startLeft + deltaX)); const newX = Math.max(0, startLeft + deltaX);
const newY = snapTo10(Math.max(0, startTop + deltaY)); const newY = Math.max(0, startTop + deltaY);
// React 상태로 위치 업데이트 (리렌더링 트리거) // React 상태로 위치 업데이트 (리렌더링 트리거)
setDragPosition({ x: newX, y: newY }); setDragPosition({ x: newX, y: newY });
@ -129,9 +126,9 @@ const TabsDesignEditor: React.FC<{
const deltaX = upEvent.clientX - startMouseX; const deltaX = upEvent.clientX - startMouseX;
const deltaY = upEvent.clientY - startMouseY; const deltaY = upEvent.clientY - startMouseY;
// 새 위치 = 시작 위치 + 이동량 (10px 단위 스냅 적용) // 새 위치 = 시작 위치 + 이동량
const newX = snapTo10(Math.max(0, startLeft + deltaX)); const newX = Math.max(0, startLeft + deltaX);
const newY = snapTo10(Math.max(0, startTop + deltaY)); const newY = Math.max(0, startTop + deltaY);
setDraggingCompId(null); setDraggingCompId(null);
setDragPosition(null); setDragPosition(null);
@ -147,8 +144,8 @@ const TabsDesignEditor: React.FC<{
? { ? {
...c, ...c,
position: { position: {
x: newX, x: Math.max(0, Math.round(newX)),
y: newY, y: Math.max(0, Math.round(newY)),
}, },
} }
: c : c
@ -175,9 +172,12 @@ const TabsDesignEditor: React.FC<{
document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
}, },
[activeTabId, component, onUpdateComponent, tabs, snapTo10] [activeTabId, component, onUpdateComponent, tabs]
); );
// 10px 단위 스냅 함수
const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []);
// 리사이즈 시작 핸들러 // 리사이즈 시작 핸들러
const handleResizeStart = useCallback( const handleResizeStart = useCallback(
(e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => { (e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => {