merge: origin/main을 ksh로 머지 (UnifiedPropertiesPanel 충돌 해결)
This commit is contained in:
@@ -53,6 +53,8 @@ interface InteractiveScreenViewerProps {
|
||||
disabledFields?: string[];
|
||||
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||
isInModal?: boolean;
|
||||
// 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
|
||||
originalData?: Record<string, any> | null;
|
||||
}
|
||||
|
||||
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
|
||||
@@ -72,6 +74,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
groupedData,
|
||||
disabledFields = [],
|
||||
isInModal = false,
|
||||
originalData, // 🆕 원본 데이터 (수정 모드에서 UPDATE 판단용)
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
const { userName: authUserName, user: authUser } = useAuth();
|
||||
@@ -331,6 +334,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
component={comp}
|
||||
isInteractive={true}
|
||||
formData={formData}
|
||||
originalData={originalData || undefined} // 🆕 원본 데이터 전달 (UPDATE 판단용)
|
||||
onFormDataChange={handleFormDataChange}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
|
||||
@@ -528,9 +528,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
|
||||
// 🆕 size 변경 시 style도 함께 업데이트 (파란 테두리와 실제 크기 동기화)
|
||||
if (path === "size.width" || path === "size.height" || path === "size") {
|
||||
if (!newComp.style) {
|
||||
newComp.style = {};
|
||||
}
|
||||
// 🔧 style 객체를 새로 복사하여 불변성 유지
|
||||
newComp.style = { ...(newComp.style || {}) };
|
||||
|
||||
if (path === "size.width") {
|
||||
newComp.style.width = `${value}px`;
|
||||
@@ -839,18 +838,46 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
// 화면의 기본 테이블/REST API 정보 로드
|
||||
useEffect(() => {
|
||||
const loadScreenDataSource = async () => {
|
||||
console.log("🔍 [ScreenDesigner] 데이터 소스 로드 시작:", {
|
||||
screenId: selectedScreen?.screenId,
|
||||
screenName: selectedScreen?.screenName,
|
||||
dataSourceType: selectedScreen?.dataSourceType,
|
||||
tableName: selectedScreen?.tableName,
|
||||
restApiConnectionId: selectedScreen?.restApiConnectionId,
|
||||
restApiEndpoint: selectedScreen?.restApiEndpoint,
|
||||
restApiJsonPath: selectedScreen?.restApiJsonPath,
|
||||
});
|
||||
|
||||
// REST API 데이터 소스인 경우
|
||||
if (selectedScreen?.dataSourceType === "restapi" && selectedScreen?.restApiConnectionId) {
|
||||
// tableName이 restapi_로 시작하면 REST API로 간주
|
||||
const isRestApi = selectedScreen?.dataSourceType === "restapi" ||
|
||||
selectedScreen?.tableName?.startsWith("restapi_") ||
|
||||
selectedScreen?.tableName?.startsWith("_restapi_");
|
||||
|
||||
if (isRestApi && (selectedScreen?.restApiConnectionId || selectedScreen?.tableName)) {
|
||||
try {
|
||||
// 연결 ID 추출 (restApiConnectionId가 없으면 tableName에서 추출)
|
||||
let connectionId = selectedScreen?.restApiConnectionId;
|
||||
if (!connectionId && selectedScreen?.tableName) {
|
||||
const match = selectedScreen.tableName.match(/restapi_(\d+)/);
|
||||
connectionId = match ? parseInt(match[1]) : undefined;
|
||||
}
|
||||
|
||||
if (!connectionId) {
|
||||
throw new Error("REST API 연결 ID를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
console.log("🌐 [ScreenDesigner] REST API 데이터 로드:", { connectionId });
|
||||
|
||||
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||
selectedScreen.restApiConnectionId,
|
||||
selectedScreen.restApiEndpoint,
|
||||
selectedScreen.restApiJsonPath || "data",
|
||||
connectionId,
|
||||
selectedScreen?.restApiEndpoint,
|
||||
selectedScreen?.restApiJsonPath || "response", // 기본값을 response로 변경
|
||||
);
|
||||
|
||||
// REST API 응답에서 컬럼 정보 생성
|
||||
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
tableName: `restapi_${connectionId}`,
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel,
|
||||
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
||||
@@ -862,10 +889,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
}));
|
||||
|
||||
const tableInfo: TableInfo = {
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
tableName: `restapi_${connectionId}`,
|
||||
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
||||
columns,
|
||||
};
|
||||
|
||||
console.log("✅ [ScreenDesigner] REST API 컬럼 로드 완료:", {
|
||||
tableName: tableInfo.tableName,
|
||||
tableLabel: tableInfo.tableLabel,
|
||||
columnsCount: columns.length,
|
||||
columns: columns.map(c => c.columnName),
|
||||
});
|
||||
|
||||
setTables([tableInfo]);
|
||||
console.log("REST API 데이터 소스 로드 완료:", {
|
||||
@@ -996,6 +1030,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
// console.log("🔧 기본 해상도 적용:", defaultResolution);
|
||||
}
|
||||
|
||||
// 🔍 디버깅: 로드된 버튼 컴포넌트의 action 확인
|
||||
const buttonComponents = layoutWithDefaultGrid.components.filter(
|
||||
(c: any) => c.componentType?.startsWith("button")
|
||||
);
|
||||
console.log("🔍 [로드] 버튼 컴포넌트 action 확인:", buttonComponents.map((c: any) => ({
|
||||
id: c.id,
|
||||
type: c.componentType,
|
||||
actionType: c.componentConfig?.action?.type,
|
||||
fullAction: c.componentConfig?.action,
|
||||
})));
|
||||
|
||||
setLayout(layoutWithDefaultGrid);
|
||||
setHistory([layoutWithDefaultGrid]);
|
||||
setHistoryIndex(0);
|
||||
@@ -1453,7 +1498,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
};
|
||||
// 🔍 버튼 컴포넌트들의 action.type 확인
|
||||
const buttonComponents = layoutWithResolution.components.filter(
|
||||
(c: any) => c.type === "button" || c.type === "button-primary" || c.type === "button-secondary",
|
||||
(c: any) => c.componentType?.startsWith("button") || c.type === "button" || c.type === "button-primary",
|
||||
);
|
||||
console.log("💾 저장 시작:", {
|
||||
screenId: selectedScreen.screenId,
|
||||
@@ -1463,6 +1508,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
buttonComponents: buttonComponents.map((c: any) => ({
|
||||
id: c.id,
|
||||
type: c.type,
|
||||
componentType: c.componentType,
|
||||
text: c.componentConfig?.text,
|
||||
actionType: c.componentConfig?.action?.type,
|
||||
fullAction: c.componentConfig?.action,
|
||||
|
||||
@@ -41,6 +41,7 @@ import { cn } from "@/lib/utils";
|
||||
import { MoreHorizontal, Edit, Trash2, Copy, Eye, Plus, Search, Palette, RotateCcw, Trash, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||
import CreateScreenModal from "./CreateScreenModal";
|
||||
import CopyScreenModal from "./CopyScreenModal";
|
||||
import dynamic from "next/dynamic";
|
||||
@@ -132,10 +133,18 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
description: "",
|
||||
isActive: "Y",
|
||||
tableName: "",
|
||||
dataSourceType: "database" as "database" | "restapi",
|
||||
restApiConnectionId: null as number | null,
|
||||
restApiEndpoint: "",
|
||||
restApiJsonPath: "data",
|
||||
});
|
||||
const [tables, setTables] = useState<Array<{ tableName: string; tableLabel: string }>>([]);
|
||||
const [loadingTables, setLoadingTables] = useState(false);
|
||||
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
||||
|
||||
// REST API 연결 관련 상태 (편집용)
|
||||
const [editRestApiConnections, setEditRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||
const [editRestApiComboboxOpen, setEditRestApiComboboxOpen] = useState(false);
|
||||
|
||||
// 미리보기 관련 상태
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
@@ -272,11 +281,19 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
|
||||
const handleEdit = async (screen: ScreenDefinition) => {
|
||||
setScreenToEdit(screen);
|
||||
|
||||
// 데이터 소스 타입 결정
|
||||
const isRestApi = screen.dataSourceType === "restapi" || screen.tableName?.startsWith("_restapi_");
|
||||
|
||||
setEditFormData({
|
||||
screenName: screen.screenName,
|
||||
description: screen.description || "",
|
||||
isActive: screen.isActive,
|
||||
tableName: screen.tableName || "",
|
||||
dataSourceType: isRestApi ? "restapi" : "database",
|
||||
restApiConnectionId: (screen as any).restApiConnectionId || null,
|
||||
restApiEndpoint: (screen as any).restApiEndpoint || "",
|
||||
restApiJsonPath: (screen as any).restApiJsonPath || "data",
|
||||
});
|
||||
setEditDialogOpen(true);
|
||||
|
||||
@@ -298,14 +315,50 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
} finally {
|
||||
setLoadingTables(false);
|
||||
}
|
||||
|
||||
// REST API 연결 목록 로드
|
||||
try {
|
||||
const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" });
|
||||
setEditRestApiConnections(connections);
|
||||
} catch (error) {
|
||||
console.error("REST API 연결 목록 조회 실패:", error);
|
||||
setEditRestApiConnections([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSave = async () => {
|
||||
if (!screenToEdit) return;
|
||||
|
||||
try {
|
||||
// 데이터 소스 타입에 따라 업데이트 데이터 구성
|
||||
const updateData: any = {
|
||||
screenName: editFormData.screenName,
|
||||
description: editFormData.description,
|
||||
isActive: editFormData.isActive,
|
||||
dataSourceType: editFormData.dataSourceType,
|
||||
};
|
||||
|
||||
if (editFormData.dataSourceType === "database") {
|
||||
updateData.tableName = editFormData.tableName;
|
||||
updateData.restApiConnectionId = null;
|
||||
updateData.restApiEndpoint = null;
|
||||
updateData.restApiJsonPath = null;
|
||||
} else {
|
||||
// REST API
|
||||
updateData.tableName = `_restapi_${editFormData.restApiConnectionId}`;
|
||||
updateData.restApiConnectionId = editFormData.restApiConnectionId;
|
||||
updateData.restApiEndpoint = editFormData.restApiEndpoint;
|
||||
updateData.restApiJsonPath = editFormData.restApiJsonPath || "data";
|
||||
}
|
||||
|
||||
console.log("📤 화면 편집 저장 요청:", {
|
||||
screenId: screenToEdit.screenId,
|
||||
editFormData,
|
||||
updateData,
|
||||
});
|
||||
|
||||
// 화면 정보 업데이트 API 호출
|
||||
await screenApi.updateScreenInfo(screenToEdit.screenId, editFormData);
|
||||
await screenApi.updateScreenInfo(screenToEdit.screenId, updateData);
|
||||
|
||||
// 선택된 테이블의 라벨 찾기
|
||||
const selectedTable = tables.find((t) => t.tableName === editFormData.tableName);
|
||||
@@ -318,10 +371,11 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
? {
|
||||
...s,
|
||||
screenName: editFormData.screenName,
|
||||
tableName: editFormData.tableName,
|
||||
tableName: updateData.tableName,
|
||||
tableLabel: tableLabel,
|
||||
description: editFormData.description,
|
||||
isActive: editFormData.isActive,
|
||||
dataSourceType: editFormData.dataSourceType,
|
||||
}
|
||||
: s,
|
||||
),
|
||||
@@ -1216,65 +1270,184 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
placeholder="화면명을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 데이터 소스 타입 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-tableName">테이블 *</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 sm:h-10 sm:text-sm"
|
||||
disabled={loadingTables}
|
||||
>
|
||||
{loadingTables
|
||||
? "로딩 중..."
|
||||
: editFormData.tableName
|
||||
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
|
||||
: "테이블을 선택하세요"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={`${table.tableName} ${table.tableLabel}`}
|
||||
onSelect={() => {
|
||||
setEditFormData({ ...editFormData, tableName: table.tableName });
|
||||
setTableComboboxOpen(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.tableLabel}</span>
|
||||
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Label>데이터 소스 타입</Label>
|
||||
<Select
|
||||
value={editFormData.dataSourceType}
|
||||
onValueChange={(value: "database" | "restapi") => {
|
||||
setEditFormData({
|
||||
...editFormData,
|
||||
dataSourceType: value,
|
||||
tableName: "",
|
||||
restApiConnectionId: null,
|
||||
restApiEndpoint: "",
|
||||
restApiJsonPath: "data",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="database">데이터베이스</SelectItem>
|
||||
<SelectItem value="restapi">REST API</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 데이터베이스 선택 (database 타입인 경우) */}
|
||||
{editFormData.dataSourceType === "database" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-tableName">테이블 *</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 sm:h-10 sm:text-sm"
|
||||
disabled={loadingTables}
|
||||
>
|
||||
{loadingTables
|
||||
? "로딩 중..."
|
||||
: editFormData.tableName
|
||||
? tables.find((table) => table.tableName === editFormData.tableName)?.tableLabel || editFormData.tableName
|
||||
: "테이블을 선택하세요"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">
|
||||
테이블을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={`${table.tableName} ${table.tableLabel}`}
|
||||
onSelect={() => {
|
||||
setEditFormData({ ...editFormData, tableName: table.tableName });
|
||||
setTableComboboxOpen(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
editFormData.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.tableLabel}</span>
|
||||
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* REST API 선택 (restapi 타입인 경우) */}
|
||||
{editFormData.dataSourceType === "restapi" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>REST API 연결 *</Label>
|
||||
<Popover open={editRestApiComboboxOpen} onOpenChange={setEditRestApiComboboxOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={editRestApiComboboxOpen}
|
||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
{editFormData.restApiConnectionId
|
||||
? editRestApiConnections.find((c) => c.id === editFormData.restApiConnectionId)?.connection_name || "선택된 연결"
|
||||
: "REST API 연결 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
||||
<CommandList>
|
||||
<CommandEmpty className="text-xs sm:text-sm">
|
||||
연결을 찾을 수 없습니다.
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{editRestApiConnections.map((conn) => (
|
||||
<CommandItem
|
||||
key={conn.id}
|
||||
value={conn.connection_name}
|
||||
onSelect={() => {
|
||||
setEditFormData({ ...editFormData, restApiConnectionId: conn.id || null });
|
||||
setEditRestApiComboboxOpen(false);
|
||||
}}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
editFormData.restApiConnectionId === conn.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-[10px] text-gray-500">{conn.base_url}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-restApiEndpoint">API 엔드포인트</Label>
|
||||
<Input
|
||||
id="edit-restApiEndpoint"
|
||||
value={editFormData.restApiEndpoint}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, restApiEndpoint: e.target.value })}
|
||||
placeholder="예: /api/data/list"
|
||||
/>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
데이터를 조회할 API 엔드포인트 경로입니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-restApiJsonPath">JSON 경로</Label>
|
||||
<Input
|
||||
id="edit-restApiJsonPath"
|
||||
value={editFormData.restApiJsonPath}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, restApiJsonPath: e.target.value })}
|
||||
placeholder="예: data 또는 result.items"
|
||||
/>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
응답 JSON에서 데이터 배열의 경로입니다 (기본: data)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-description">설명</Label>
|
||||
<Textarea
|
||||
@@ -1305,7 +1478,14 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr
|
||||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleEditSave} disabled={!editFormData.screenName.trim() || !editFormData.tableName.trim()}>
|
||||
<Button
|
||||
onClick={handleEditSave}
|
||||
disabled={
|
||||
!editFormData.screenName.trim() ||
|
||||
(editFormData.dataSourceType === "database" && !editFormData.tableName.trim()) ||
|
||||
(editFormData.dataSourceType === "restapi" && !editFormData.restApiConnectionId)
|
||||
}
|
||||
>
|
||||
저장
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -740,6 +740,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
const handleConfigChange = (newConfig: WebTypeConfig) => {
|
||||
// 강제 새 객체 생성으로 React 변경 감지 보장
|
||||
const freshConfig = { ...newConfig };
|
||||
console.log("🔧 [DetailSettingsPanel] handleConfigChange 호출:", {
|
||||
widgetId: widget.id,
|
||||
widgetLabel: widget.label,
|
||||
widgetType: widget.widgetType,
|
||||
newConfig: freshConfig,
|
||||
});
|
||||
onUpdateProperty(widget.id, "webTypeConfig", freshConfig);
|
||||
|
||||
// TextTypeConfig의 자동입력 설정을 autoGeneration으로도 매핑
|
||||
|
||||
@@ -114,7 +114,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
}) => {
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||
|
||||
|
||||
// 높이/너비 입력 로컬 상태 (자유 입력 허용)
|
||||
const [localHeight, setLocalHeight] = useState<string>("");
|
||||
const [localWidth, setLocalWidth] = useState<string>("");
|
||||
@@ -147,7 +147,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
}
|
||||
}
|
||||
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
||||
|
||||
|
||||
// 높이 값 동기화
|
||||
useEffect(() => {
|
||||
if (selectedComponent?.size?.height !== undefined) {
|
||||
@@ -179,7 +179,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
// 최대 컬럼 수 계산
|
||||
const MIN_COLUMN_WIDTH = 30;
|
||||
const maxColumns = currentResolution
|
||||
? Math.floor((currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) / (MIN_COLUMN_WIDTH + gridSettings.gap))
|
||||
? Math.floor(
|
||||
(currentResolution.width - gridSettings.padding * 2 + gridSettings.gap) /
|
||||
(MIN_COLUMN_WIDTH + gridSettings.gap),
|
||||
)
|
||||
: 24;
|
||||
const safeMaxColumns = Math.max(1, Math.min(maxColumns, 100)); // 최대 100개로 제한
|
||||
|
||||
@@ -189,7 +192,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
<Grid3X3 className="text-primary h-3 w-3" />
|
||||
<h4 className="text-xs font-semibold">격자 설정</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* 토글들 */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -226,9 +229,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
|
||||
{/* 10px 단위 스냅 안내 */}
|
||||
<div className="bg-muted/50 rounded-md p-2">
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
모든 컴포넌트는 10px 단위로 자동 배치됩니다.
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px]">모든 컴포넌트는 10px 단위로 자동 배치됩니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,9 +239,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
// 컴포넌트가 선택되지 않았을 때도 해상도 설정과 격자 설정은 표시
|
||||
if (!selectedComponent) {
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white overflow-hidden">
|
||||
<div className="flex h-full flex-col overflow-x-auto bg-white">
|
||||
{/* 해상도 설정과 격자 설정 표시 */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||
<div className="flex-1 overflow-x-auto overflow-y-auto p-2">
|
||||
<div className="space-y-4 text-xs">
|
||||
{/* 해상도 설정 */}
|
||||
{currentResolution && onResolutionChange && (
|
||||
@@ -287,9 +288,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
if (!selectedComponent) return null;
|
||||
|
||||
// 🎯 Section Card, Section Paper 등 신규 컴포넌트는 componentType에서 감지
|
||||
const componentType =
|
||||
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
|
||||
selectedComponent.componentConfig?.type ||
|
||||
const componentType =
|
||||
selectedComponent.componentType || // ⭐ 1순위: ScreenDesigner가 설정한 componentType (section-card 등)
|
||||
selectedComponent.componentConfig?.type ||
|
||||
selectedComponent.componentConfig?.id ||
|
||||
selectedComponent.type;
|
||||
|
||||
@@ -305,15 +306,15 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
};
|
||||
|
||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기 시도
|
||||
const componentId =
|
||||
selectedComponent.componentType || // ⭐ section-card 등
|
||||
selectedComponent.componentConfig?.type ||
|
||||
const componentId =
|
||||
selectedComponent.componentType || // ⭐ section-card 등
|
||||
selectedComponent.componentConfig?.type ||
|
||||
selectedComponent.componentConfig?.id ||
|
||||
(selectedComponent.type === "component" ? selectedComponent.id : null); // 🆕 독립 컴포넌트 (table-search-widget 등)
|
||||
|
||||
|
||||
if (componentId) {
|
||||
const definition = ComponentRegistry.getComponent(componentId);
|
||||
|
||||
|
||||
if (definition?.configPanel) {
|
||||
const ConfigPanelComponent = definition.configPanel;
|
||||
const currentConfig = selectedComponent.componentConfig || {};
|
||||
@@ -325,29 +326,41 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
currentConfig,
|
||||
});
|
||||
|
||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
// 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지)
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
|
||||
const handlePanelConfigChange = (newConfig: any) => {
|
||||
// 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합
|
||||
const mergedConfig = {
|
||||
...currentConfig, // 기존 설정 유지
|
||||
...newConfig, // 새 설정 병합
|
||||
};
|
||||
console.log("🔧 [ConfigPanel] handleConfigChange:", {
|
||||
componentId: selectedComponent.id,
|
||||
currentConfig,
|
||||
newConfig,
|
||||
mergedConfig,
|
||||
});
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", mergedConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4" key={selectedComponent.id}>
|
||||
<div key={selectedComponent.id} className="space-y-4">
|
||||
<div className="flex items-center gap-2 border-b pb-2">
|
||||
<Settings className="h-4 w-4 text-primary" />
|
||||
<Settings className="text-primary h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||
</div>
|
||||
<Suspense fallback={
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-sm text-muted-foreground">설정 패널 로딩 중...</div>
|
||||
</div>
|
||||
}>
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={handleConfigChange}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground text-sm">설정 패널 로딩 중...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={handlePanelConfigChange}
|
||||
onConfigChange={handlePanelConfigChange} // 🔧 autocomplete-search-input 등 일부 컴포넌트용
|
||||
tables={tables} // 테이블 정보 전달
|
||||
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
|
||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName} // 🔧 화면 테이블명 전달
|
||||
@@ -414,9 +427,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
<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>
|
||||
<p className="text-muted-foreground text-xs">제목과 테두리가 있는 명확한 그룹화 컨테이너</p>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 */}
|
||||
@@ -428,7 +439,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.showHeader", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="showHeader" className="text-xs cursor-pointer">
|
||||
<Label htmlFor="showHeader" className="cursor-pointer text-xs">
|
||||
헤더 표시
|
||||
</Label>
|
||||
</div>
|
||||
@@ -458,7 +469,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.description", e.target.value);
|
||||
}}
|
||||
placeholder="섹션 설명 입력"
|
||||
className="text-xs resize-none"
|
||||
className="resize-none text-xs"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
@@ -526,7 +537,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 접기/펼치기 기능 */}
|
||||
<div className="space-y-2 pt-2 border-t">
|
||||
<div className="space-y-2 border-t pt-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="collapsible"
|
||||
@@ -535,13 +546,13 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.collapsible", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="collapsible" className="text-xs cursor-pointer">
|
||||
<Label htmlFor="collapsible" className="cursor-pointer text-xs">
|
||||
접기/펼치기 가능
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{selectedComponent.componentConfig?.collapsible && (
|
||||
<div className="flex items-center space-x-2 ml-6">
|
||||
<div className="ml-6 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="defaultOpen"
|
||||
checked={selectedComponent.componentConfig?.defaultOpen !== false}
|
||||
@@ -549,7 +560,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.defaultOpen", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="defaultOpen" className="text-xs cursor-pointer">
|
||||
<Label htmlFor="defaultOpen" className="cursor-pointer text-xs">
|
||||
기본으로 펼치기
|
||||
</Label>
|
||||
</div>
|
||||
@@ -563,9 +574,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
<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>
|
||||
<p className="text-muted-foreground text-xs">배경색 기반의 미니멀한 그룹화 컨테이너</p>
|
||||
</div>
|
||||
|
||||
{/* 배경색 */}
|
||||
@@ -676,7 +685,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
handleUpdateProperty(selectedComponent.id, "componentConfig.showBorder", checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="showBorder" className="text-xs cursor-pointer">
|
||||
<Label htmlFor="showBorder" className="cursor-pointer text-xs">
|
||||
미묘한 테두리 표시
|
||||
</Label>
|
||||
</div>
|
||||
@@ -687,9 +696,9 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
// ConfigPanel이 없는 경우 경고 표시
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
||||
<Settings className="mb-4 h-12 w-12 text-muted-foreground" />
|
||||
<Settings className="text-muted-foreground mb-4 h-12 w-12" />
|
||||
<h3 className="mb-2 text-base font-medium">⚠️ 설정 패널 없음</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
컴포넌트 "{componentId || componentType}"에 대한 설정 패널이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
@@ -1403,7 +1412,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white overflow-hidden">
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
{/* 헤더 - 간소화 */}
|
||||
<div className="border-border border-b px-3 py-2">
|
||||
{selectedComponent.type === "widget" && (
|
||||
@@ -1414,7 +1423,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 통합 컨텐츠 (탭 제거) */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
|
||||
<div className="flex-1 overflow-x-auto overflow-y-auto p-2">
|
||||
<div className="space-y-4 text-xs">
|
||||
{/* 해상도 설정 - 항상 맨 위에 표시 */}
|
||||
{currentResolution && onResolutionChange && (
|
||||
|
||||
Reference in New Issue
Block a user