Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
ResizableDialogContent,
|
||||
ResizableDialogHeader,
|
||||
ResizableDialogTitle,
|
||||
ResizableDialogDescription,
|
||||
ResizableDialogFooter,
|
||||
} from "@/components/ui/resizable-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,11 +14,13 @@ import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Search, X, Check, ChevronsUpDown, Database } from "lucide-react";
|
||||
import { Search, X, Check, ChevronsUpDown, Database, Globe } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||
import { ScreenDefinition } from "@/types/screen";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||
|
||||
interface CreateScreenModalProps {
|
||||
open: boolean;
|
||||
@@ -39,12 +40,22 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
const [tableSearchTerm, setTableSearchTerm] = useState("");
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 데이터 소스 타입 (database: 데이터베이스, restapi: REST API)
|
||||
const [dataSourceType, setDataSourceType] = useState<"database" | "restapi">("database");
|
||||
|
||||
// 외부 DB 연결 관련 상태
|
||||
const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal");
|
||||
const [externalConnections, setExternalConnections] = useState<any[]>([]);
|
||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
||||
const [openDbSourceCombobox, setOpenDbSourceCombobox] = useState(false);
|
||||
|
||||
// REST API 연결 관련 상태
|
||||
const [restApiConnections, setRestApiConnections] = useState<ExternalRestApiConnection[]>([]);
|
||||
const [selectedRestApiId, setSelectedRestApiId] = useState<number | null>(null);
|
||||
const [openRestApiCombobox, setOpenRestApiCombobox] = useState(false);
|
||||
const [restApiEndpoint, setRestApiEndpoint] = useState("");
|
||||
const [restApiJsonPath, setRestApiJsonPath] = useState("data"); // 응답에서 데이터 추출 경로
|
||||
// 화면 코드 자동 생성
|
||||
const generateCode = async () => {
|
||||
try {
|
||||
@@ -109,6 +120,21 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
loadConnections();
|
||||
}, [open]);
|
||||
|
||||
// REST API 연결 목록 로드
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const loadRestApiConnections = async () => {
|
||||
try {
|
||||
const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" });
|
||||
setRestApiConnections(connections);
|
||||
} catch (error) {
|
||||
console.error("Failed to load REST API connections:", error);
|
||||
setRestApiConnections([]);
|
||||
}
|
||||
};
|
||||
loadRestApiConnections();
|
||||
}, [open]);
|
||||
|
||||
// 외부 DB 테이블 목록 로드
|
||||
useEffect(() => {
|
||||
if (selectedDbSource === "internal" || !selectedDbSource) {
|
||||
@@ -160,8 +186,15 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
}, [open, screenCode]);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
return screenName.trim().length > 0 && screenCode.trim().length > 0 && tableName.trim().length > 0;
|
||||
}, [screenName, screenCode, tableName]);
|
||||
const baseValid = screenName.trim().length > 0 && screenCode.trim().length > 0;
|
||||
|
||||
if (dataSourceType === "database") {
|
||||
return baseValid && tableName.trim().length > 0;
|
||||
} else {
|
||||
// REST API: 연결 선택 필수
|
||||
return baseValid && selectedRestApiId !== null;
|
||||
}
|
||||
}, [screenName, screenCode, tableName, dataSourceType, selectedRestApiId]);
|
||||
|
||||
// 테이블 필터링 (내부 DB용)
|
||||
const filteredTables = useMemo(() => {
|
||||
@@ -186,17 +219,30 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
setSubmitting(true);
|
||||
const companyCode = (user as any)?.company_code || (user as any)?.companyCode || "*";
|
||||
|
||||
// DB 소스 정보 추가
|
||||
const created = await screenApi.createScreen({
|
||||
// 데이터 소스 타입에 따라 다른 정보 전달
|
||||
const createData: any = {
|
||||
screenName: screenName.trim(),
|
||||
screenCode: screenCode.trim(),
|
||||
tableName: tableName.trim(),
|
||||
companyCode,
|
||||
description: description.trim() || undefined,
|
||||
createdBy: (user as any)?.userId,
|
||||
dbSourceType: selectedDbSource === "internal" ? "internal" : "external",
|
||||
dbConnectionId: selectedDbSource === "internal" ? undefined : Number(selectedDbSource),
|
||||
} as any);
|
||||
dataSourceType: dataSourceType,
|
||||
};
|
||||
|
||||
if (dataSourceType === "database") {
|
||||
// 데이터베이스 소스
|
||||
createData.tableName = tableName.trim();
|
||||
createData.dbSourceType = selectedDbSource === "internal" ? "internal" : "external";
|
||||
createData.dbConnectionId = selectedDbSource === "internal" ? undefined : Number(selectedDbSource);
|
||||
} else {
|
||||
// REST API 소스
|
||||
createData.tableName = `_restapi_${selectedRestApiId}`; // REST API용 가상 테이블명
|
||||
createData.restApiConnectionId = selectedRestApiId;
|
||||
createData.restApiEndpoint = restApiEndpoint.trim() || undefined;
|
||||
createData.restApiJsonPath = restApiJsonPath.trim() || "data";
|
||||
}
|
||||
|
||||
const created = await screenApi.createScreen(createData);
|
||||
|
||||
// 날짜 필드 보정
|
||||
const mapped: ScreenDefinition = {
|
||||
@@ -207,11 +253,16 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
|
||||
onCreated?.(mapped);
|
||||
onOpenChange(false);
|
||||
// 폼 초기화
|
||||
setScreenName("");
|
||||
setScreenCode("");
|
||||
setTableName("");
|
||||
setDescription("");
|
||||
setSelectedDbSource("internal");
|
||||
setDataSourceType("database");
|
||||
setSelectedRestApiId(null);
|
||||
setRestApiEndpoint("");
|
||||
setRestApiJsonPath("data");
|
||||
} catch (e) {
|
||||
// 필요 시 토스트 추가 가능
|
||||
} finally {
|
||||
@@ -263,83 +314,210 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DB 소스 선택 */}
|
||||
{/* 데이터 소스 타입 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dbSource">데이터베이스 소스</Label>
|
||||
<Popover open={openDbSourceCombobox} onOpenChange={setOpenDbSourceCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDbSourceCombobox}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
{selectedDbSource === "internal"
|
||||
? "내부 데이터베이스"
|
||||
: externalConnections.find((conn) => conn.id === selectedDbSource)?.connection_name ||
|
||||
"선택하세요"}
|
||||
</div>
|
||||
<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="데이터베이스 검색..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>데이터베이스를 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="internal"
|
||||
onSelect={() => {
|
||||
setSelectedDbSource("internal");
|
||||
setTableName("");
|
||||
setTableSearchTerm("");
|
||||
setOpenDbSourceCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", selectedDbSource === "internal" ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Database className="mr-2 h-4 w-4 text-blue-500" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">내부 데이터베이스</span>
|
||||
<span className="text-xs text-gray-500">PostgreSQL (현재 시스템)</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
{externalConnections.map((conn: any) => (
|
||||
<CommandItem
|
||||
key={conn.id}
|
||||
value={`${conn.connection_name} ${conn.db_type}`}
|
||||
onSelect={() => {
|
||||
setSelectedDbSource(conn.id);
|
||||
setTableName("");
|
||||
setTableSearchTerm("");
|
||||
setOpenDbSourceCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", selectedDbSource === conn.id ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Database className="mr-2 h-4 w-4 text-green-500" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-xs text-gray-500">{conn.db_type?.toUpperCase()}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-xs text-gray-500">화면에서 사용할 데이터베이스를 선택합니다</p>
|
||||
<Label>데이터 소스 타입</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant={dataSourceType === "database" ? "default" : "outline"}
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setDataSourceType("database");
|
||||
setSelectedRestApiId(null);
|
||||
}}
|
||||
>
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
데이터베이스
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={dataSourceType === "restapi" ? "default" : "outline"}
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setDataSourceType("restapi");
|
||||
setTableName("");
|
||||
setSelectedDbSource("internal");
|
||||
}}
|
||||
>
|
||||
<Globe className="mr-2 h-4 w-4" />
|
||||
REST API
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
{/* 데이터베이스 소스 설정 */}
|
||||
{dataSourceType === "database" && (
|
||||
<>
|
||||
{/* DB 소스 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dbSource">데이터베이스 소스</Label>
|
||||
<Popover open={openDbSourceCombobox} onOpenChange={setOpenDbSourceCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDbSourceCombobox}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
{selectedDbSource === "internal"
|
||||
? "내부 데이터베이스"
|
||||
: externalConnections.find((conn) => conn.id === selectedDbSource)?.connection_name ||
|
||||
"선택하세요"}
|
||||
</div>
|
||||
<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="데이터베이스 검색..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>데이터베이스를 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
value="internal"
|
||||
onSelect={() => {
|
||||
setSelectedDbSource("internal");
|
||||
setTableName("");
|
||||
setTableSearchTerm("");
|
||||
setOpenDbSourceCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", selectedDbSource === "internal" ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Database className="mr-2 h-4 w-4 text-blue-500" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">내부 데이터베이스</span>
|
||||
<span className="text-xs text-gray-500">PostgreSQL (현재 시스템)</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
{externalConnections.map((conn: any) => (
|
||||
<CommandItem
|
||||
key={conn.id}
|
||||
value={`${conn.connection_name} ${conn.db_type}`}
|
||||
onSelect={() => {
|
||||
setSelectedDbSource(conn.id);
|
||||
setTableName("");
|
||||
setTableSearchTerm("");
|
||||
setOpenDbSourceCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", selectedDbSource === conn.id ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Database className="mr-2 h-4 w-4 text-green-500" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-xs text-gray-500">{conn.db_type?.toUpperCase()}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-xs text-gray-500">화면에서 사용할 데이터베이스를 선택합니다</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* REST API 소스 설정 */}
|
||||
{dataSourceType === "restapi" && (
|
||||
<>
|
||||
{/* REST API 연결 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="restApiConnection">REST API 연결 *</Label>
|
||||
<Popover open={openRestApiCombobox} onOpenChange={setOpenRestApiCombobox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openRestApiCombobox}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
{selectedRestApiId
|
||||
? restApiConnections.find((conn) => conn.id === selectedRestApiId)?.connection_name ||
|
||||
"선택하세요"
|
||||
: "REST API 연결을 선택하세요"}
|
||||
</div>
|
||||
<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="REST API 검색..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>등록된 REST API 연결이 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{restApiConnections.map((conn) => (
|
||||
<CommandItem
|
||||
key={conn.id}
|
||||
value={`${conn.connection_name} ${conn.base_url}`}
|
||||
onSelect={() => {
|
||||
setSelectedRestApiId(conn.id!);
|
||||
setRestApiEndpoint(conn.endpoint_path || "");
|
||||
setOpenRestApiCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", selectedRestApiId === conn.id ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<Globe className="mr-2 h-4 w-4 text-purple-500" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-xs text-gray-500 truncate max-w-[300px]">{conn.base_url}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="text-xs text-gray-500">
|
||||
등록된 REST API 연결을 선택합니다.
|
||||
<Link href="/admin/externalRestApi" className="ml-1 text-primary hover:underline">
|
||||
새 연결 등록
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 엔드포인트 경로 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="restApiEndpoint">엔드포인트 경로</Label>
|
||||
<Input
|
||||
id="restApiEndpoint"
|
||||
value={restApiEndpoint}
|
||||
onChange={(e) => setRestApiEndpoint(e.target.value)}
|
||||
placeholder="/api/data 또는 /users"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">기본 URL 뒤에 추가될 경로 (선택사항)</p>
|
||||
</div>
|
||||
|
||||
{/* JSON Path */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="restApiJsonPath">데이터 경로 (JSON Path)</Label>
|
||||
<Input
|
||||
id="restApiJsonPath"
|
||||
value={restApiJsonPath}
|
||||
onChange={(e) => setRestApiJsonPath(e.target.value)}
|
||||
placeholder="data 또는 result.items"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">API 응답에서 데이터 배열을 추출할 경로 (예: data, result.items)</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 테이블 선택 (데이터베이스 모드일 때만) */}
|
||||
{dataSourceType === "database" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tableName">테이블</Label>
|
||||
<Label htmlFor="tableName">테이블 *</Label>
|
||||
<Select
|
||||
value={tableName}
|
||||
onValueChange={setTableName}
|
||||
@@ -422,11 +600,7 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Input id="description" value={description} onChange={(e) => setDescription(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ResizableDialogFooter className="mt-4">
|
||||
|
||||
@@ -408,6 +408,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
value: currentValue,
|
||||
onChange: (value: any) => handleFormDataChange(fieldName, value),
|
||||
onFormDataChange: handleFormDataChange,
|
||||
formData: formData, // 🆕 전체 formData 전달
|
||||
isInteractive: true,
|
||||
readonly: readonly,
|
||||
required: required,
|
||||
@@ -415,6 +416,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
className: "w-full h-full",
|
||||
isInModal: isInModal, // 🆕 EditModal 내부 여부 전달
|
||||
onSave: onSave, // 🆕 EditModal의 handleSave 콜백 전달
|
||||
groupedData: groupedData, // 🆕 그룹 데이터 전달 (RepeatScreenModal용)
|
||||
}}
|
||||
config={widget.webTypeConfig}
|
||||
onEvent={(event: string, data: any) => {
|
||||
|
||||
@@ -66,6 +66,7 @@ const calculateGridInfo = (width: number, height: number, settings: any) => {
|
||||
import { GroupingToolbar } from "./GroupingToolbar";
|
||||
import { screenApi, tableTypeApi } from "@/lib/api/screen";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection";
|
||||
import { toast } from "sonner";
|
||||
import { MenuAssignmentModal } from "./MenuAssignmentModal";
|
||||
import { FileAttachmentDetailModal } from "./FileAttachmentDetailModal";
|
||||
@@ -834,9 +835,52 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 화면의 기본 테이블 정보 로드 (원래대로 복원)
|
||||
// 화면의 기본 테이블/REST API 정보 로드
|
||||
useEffect(() => {
|
||||
const loadScreenTable = async () => {
|
||||
const loadScreenDataSource = async () => {
|
||||
// REST API 데이터 소스인 경우
|
||||
if (selectedScreen?.dataSourceType === "restapi" && selectedScreen?.restApiConnectionId) {
|
||||
try {
|
||||
const restApiData = await ExternalRestApiConnectionAPI.fetchData(
|
||||
selectedScreen.restApiConnectionId,
|
||||
selectedScreen.restApiEndpoint,
|
||||
selectedScreen.restApiJsonPath || "data",
|
||||
);
|
||||
|
||||
// REST API 응답에서 컬럼 정보 생성
|
||||
const columns: ColumnInfo[] = restApiData.columns.map((col) => ({
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel,
|
||||
dataType: col.dataType === "string" ? "varchar" : col.dataType === "number" ? "numeric" : col.dataType,
|
||||
webType: col.dataType === "number" ? "number" : "text",
|
||||
input_type: "text",
|
||||
widgetType: col.dataType === "number" ? "number" : "text",
|
||||
isNullable: "YES",
|
||||
required: false,
|
||||
}));
|
||||
|
||||
const tableInfo: TableInfo = {
|
||||
tableName: `restapi_${selectedScreen.restApiConnectionId}`,
|
||||
tableLabel: restApiData.connectionInfo.connectionName || "REST API 데이터",
|
||||
columns,
|
||||
};
|
||||
|
||||
setTables([tableInfo]);
|
||||
console.log("REST API 데이터 소스 로드 완료:", {
|
||||
connectionName: restApiData.connectionInfo.connectionName,
|
||||
columnsCount: columns.length,
|
||||
rowsCount: restApiData.total,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("REST API 데이터 소스 로드 실패:", error);
|
||||
toast.error("REST API 데이터를 불러오는데 실패했습니다.");
|
||||
setTables([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터베이스 데이터 소스인 경우 (기존 로직)
|
||||
const tableName = selectedScreen?.tableName;
|
||||
if (!tableName) {
|
||||
setTables([]);
|
||||
@@ -858,16 +902,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => {
|
||||
const widgetType = col.widgetType || col.widget_type || col.webType || col.web_type;
|
||||
|
||||
// 🔍 이미지 타입 디버깅
|
||||
// if (widgetType === "image" || col.webType === "image" || col.web_type === "image") {
|
||||
// console.log("🖼️ 이미지 컬럼 발견:", {
|
||||
// columnName: col.columnName || col.column_name,
|
||||
// widgetType,
|
||||
// webType: col.webType || col.web_type,
|
||||
// rawData: col,
|
||||
// });
|
||||
// }
|
||||
|
||||
return {
|
||||
tableName: col.tableName || tableName,
|
||||
columnName: col.columnName || col.column_name,
|
||||
@@ -898,8 +932,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
}
|
||||
};
|
||||
|
||||
loadScreenTable();
|
||||
}, [selectedScreen?.tableName, selectedScreen?.screenName]);
|
||||
loadScreenDataSource();
|
||||
}, [selectedScreen?.tableName, selectedScreen?.screenName, selectedScreen?.dataSourceType, selectedScreen?.restApiConnectionId, selectedScreen?.restApiEndpoint, selectedScreen?.restApiJsonPath]);
|
||||
|
||||
// 화면 레이아웃 로드
|
||||
useEffect(() => {
|
||||
|
||||
@@ -869,27 +869,23 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
});
|
||||
|
||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||
const ConfigPanelWrapper = () => {
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 border-b pb-2">
|
||||
<Settings className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||
</div>
|
||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||
</div>
|
||||
);
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
};
|
||||
|
||||
return <ConfigPanelWrapper key={selectedComponent.id} />;
|
||||
return (
|
||||
<div className="space-y-4" key={selectedComponent.id}>
|
||||
<div className="flex items-center gap-2 border-b pb-2">
|
||||
<Settings className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold">{definition.name} 설정</h3>
|
||||
</div>
|
||||
<ConfigPanelComponent config={config} onChange={handleConfigChange} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn("⚠️ ConfigPanel 없음:", {
|
||||
componentId,
|
||||
|
||||
@@ -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">
|
||||
<div className="flex h-full flex-col overflow-x-auto bg-white">
|
||||
{/* 해상도 설정과 격자 설정 표시 */}
|
||||
<div className="flex-1 overflow-y-auto 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 || {};
|
||||
@@ -327,12 +328,12 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
|
||||
// 🔧 ConfigPanelWrapper를 인라인 함수 대신 직접 JSX 반환 (리마운트 방지)
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
|
||||
const handlePanelConfigChange = (newConfig: any) => {
|
||||
// 🔧 Partial 업데이트: 기존 componentConfig를 유지하면서 새 설정만 병합
|
||||
const mergedConfig = {
|
||||
...currentConfig, // 기존 설정 유지
|
||||
...newConfig, // 새 설정 병합
|
||||
...currentConfig, // 기존 설정 유지
|
||||
...newConfig, // 새 설정 병합
|
||||
};
|
||||
console.log("🔧 [ConfigPanel] handleConfigChange:", {
|
||||
componentId: selectedComponent.id,
|
||||
@@ -346,16 +347,18 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
return (
|
||||
<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}
|
||||
<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}
|
||||
tables={tables} // 테이블 정보 전달
|
||||
allTables={allTables} // 🆕 전체 테이블 목록 전달 (selected-items-detail-input 등에서 사용)
|
||||
@@ -423,9 +426,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>
|
||||
|
||||
{/* 헤더 표시 */}
|
||||
@@ -437,7 +438,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>
|
||||
@@ -467,7 +468,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>
|
||||
@@ -535,7 +536,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"
|
||||
@@ -544,13 +545,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}
|
||||
@@ -558,7 +559,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>
|
||||
@@ -572,9 +573,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>
|
||||
|
||||
{/* 배경색 */}
|
||||
@@ -685,7 +684,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>
|
||||
@@ -696,9 +695,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>
|
||||
@@ -1423,7 +1422,7 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 통합 컨텐츠 (탭 제거) */}
|
||||
<div className="flex-1 overflow-y-auto 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