반응형 및 테이블 리스트 컴포넌트 오류 수정

This commit is contained in:
kjs 2025-10-17 15:31:23 +09:00
parent 54e9f45823
commit 2a8081a253
21 changed files with 886 additions and 262 deletions

View File

@ -515,6 +515,7 @@ export class DashboardController {
}); });
// 외부 API 호출 // 외부 API 호출
// @ts-ignore - node-fetch dynamic import
const fetch = (await import("node-fetch")).default; const fetch = (await import("node-fetch")).default;
const response = await fetch(urlObj.toString(), { const response = await fetch(urlObj.toString(), {
method: method.toUpperCase(), method: method.toUpperCase(),

View File

@ -295,6 +295,54 @@ export class DynamicFormService {
} }
}); });
// 📝 RepeaterInput 데이터 처리 (JSON 배열을 개별 레코드로 분해)
const repeaterData: Array<{
data: Record<string, any>[];
targetTable?: string;
componentId: string;
}> = [];
Object.keys(dataToInsert).forEach((key) => {
const value = dataToInsert[key];
// RepeaterInput 데이터인지 확인 (JSON 배열 문자열)
if (
typeof value === "string" &&
value.trim().startsWith("[") &&
value.trim().endsWith("]")
) {
try {
const parsedArray = JSON.parse(value);
if (Array.isArray(parsedArray) && parsedArray.length > 0) {
console.log(
`🔄 RepeaterInput 데이터 감지: ${key}, ${parsedArray.length}개 항목`
);
// 컴포넌트 설정에서 targetTable 추출 (컴포넌트 ID를 통해)
// 프론트엔드에서 { data: [...], targetTable: "..." } 형식으로 전달될 수 있음
let targetTable: string | undefined;
let actualData = parsedArray;
// 첫 번째 항목에 _targetTable이 있는지 확인 (프론트엔드에서 메타데이터 전달)
if (parsedArray[0] && parsedArray[0]._targetTable) {
targetTable = parsedArray[0]._targetTable;
actualData = parsedArray.map(
({ _targetTable, ...item }) => item
);
}
repeaterData.push({
data: actualData,
targetTable,
componentId: key,
});
delete dataToInsert[key]; // 원본 배열 데이터는 제거
}
} catch (parseError) {
console.log(`⚠️ JSON 파싱 실패: ${key}`);
}
}
});
// 존재하지 않는 컬럼 제거 // 존재하지 않는 컬럼 제거
Object.keys(dataToInsert).forEach((key) => { Object.keys(dataToInsert).forEach((key) => {
if (!tableColumns.includes(key)) { if (!tableColumns.includes(key)) {
@ -305,6 +353,9 @@ export class DynamicFormService {
} }
}); });
// RepeaterInput 데이터 처리 로직은 메인 저장 후에 처리
// (각 Repeater가 다른 테이블에 저장될 수 있으므로)
console.log("🎯 실제 테이블에 삽입할 데이터:", { console.log("🎯 실제 테이블에 삽입할 데이터:", {
tableName, tableName,
dataToInsert, dataToInsert,
@ -388,6 +439,111 @@ export class DynamicFormService {
// 결과를 표준 형식으로 변환 // 결과를 표준 형식으로 변환
const insertedRecord = Array.isArray(result) ? result[0] : result; const insertedRecord = Array.isArray(result) ? result[0] : result;
// 📝 RepeaterInput 데이터 저장 (각 Repeater를 해당 테이블에 저장)
if (repeaterData.length > 0) {
console.log(
`🔄 RepeaterInput 데이터 저장 시작: ${repeaterData.length}개 Repeater`
);
for (const repeater of repeaterData) {
const targetTableName = repeater.targetTable || tableName;
console.log(
`📝 Repeater "${repeater.componentId}" → 테이블 "${targetTableName}"에 ${repeater.data.length}개 항목 저장`
);
// 대상 테이블의 컬럼 및 기본키 정보 조회
const targetTableColumns =
await this.getTableColumns(targetTableName);
const targetPrimaryKeys = await this.getPrimaryKeys(targetTableName);
// 컬럼명만 추출
const targetColumnNames = targetTableColumns.map(
(col) => col.columnName
);
// 각 항목을 저장
for (let i = 0; i < repeater.data.length; i++) {
const item = repeater.data[i];
const itemData: Record<string, any> = {
...item,
created_by,
updated_by,
regdate: new Date(),
};
// 대상 테이블에 존재하는 컬럼만 필터링
Object.keys(itemData).forEach((key) => {
if (!targetColumnNames.includes(key)) {
delete itemData[key];
}
});
// 타입 변환 적용
Object.keys(itemData).forEach((columnName) => {
const column = targetTableColumns.find(
(col) => col.columnName === columnName
);
if (column) {
itemData[columnName] = this.convertValueForPostgreSQL(
itemData[columnName],
column.dataType
);
}
});
// UPSERT 쿼리 생성
const itemColumns = Object.keys(itemData);
const itemValues: any[] = Object.values(itemData);
const itemPlaceholders = itemValues
.map((_, index) => `$${index + 1}`)
.join(", ");
let itemUpsertQuery: string;
if (targetPrimaryKeys.length > 0) {
const conflictColumns = targetPrimaryKeys.join(", ");
const updateSet = itemColumns
.filter((col) => !targetPrimaryKeys.includes(col))
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
if (updateSet) {
itemUpsertQuery = `
INSERT INTO ${targetTableName} (${itemColumns.join(", ")})
VALUES (${itemPlaceholders})
ON CONFLICT (${conflictColumns})
DO UPDATE SET ${updateSet}
RETURNING *
`;
} else {
itemUpsertQuery = `
INSERT INTO ${targetTableName} (${itemColumns.join(", ")})
VALUES (${itemPlaceholders})
ON CONFLICT (${conflictColumns})
DO NOTHING
RETURNING *
`;
}
} else {
itemUpsertQuery = `
INSERT INTO ${targetTableName} (${itemColumns.join(", ")})
VALUES (${itemPlaceholders})
RETURNING *
`;
}
console.log(
` 📝 항목 ${i + 1}/${repeater.data.length} 저장:`,
itemData
);
await query<any>(itemUpsertQuery, itemValues);
}
console.log(` ✅ Repeater "${repeater.componentId}" 저장 완료`);
}
console.log(`✅ 모든 RepeaterInput 데이터 저장 완료`);
}
// 🔥 조건부 연결 실행 (INSERT 트리거) // 🔥 조건부 연결 실행 (INSERT 트리거)
try { try {
if (company_code) { if (company_code) {
@ -1114,6 +1270,31 @@ export class DynamicFormService {
} }
} }
/**
*
*/
async getPrimaryKeys(tableName: string): Promise<string[]> {
try {
console.log("🔑 서비스: 테이블 기본키 조회 시작:", { tableName });
const result = await query<{ column_name: string }>(
`SELECT a.attname AS column_name
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary`,
[tableName]
);
const primaryKeys = result.map((row) => row.column_name);
console.log("✅ 서비스: 테이블 기본키 조회 성공:", primaryKeys);
return primaryKeys;
} catch (error) {
console.error("❌ 서비스: 테이블 기본키 조회 실패:", error);
throw new Error(`테이블 기본키 조회 실패: ${error}`);
}
}
/** /**
* ( ) * ( )
*/ */

View File

@ -219,7 +219,11 @@ export class EntityJoinService {
]; ];
const separator = config.separator || " - "; const separator = config.separator || " - ";
if (displayColumns.length === 1) { if (displayColumns.length === 0 || !displayColumns[0]) {
// displayColumns가 빈 배열이거나 첫 번째 값이 null/undefined인 경우
// 조인 테이블의 referenceColumn을 기본값으로 사용
return `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`;
} else if (displayColumns.length === 1) {
// 단일 컬럼인 경우 // 단일 컬럼인 경우
const col = displayColumns[0]; const col = displayColumns[0];
const isJoinTableColumn = [ const isJoinTableColumn = [

View File

@ -22,7 +22,7 @@ export default function ScreenViewPage() {
const [layout, setLayout] = useState<LayoutData | null>(null); const [layout, setLayout] = useState<LayoutData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState<Record<string, any>>({}); const [formData, setFormData] = useState<Record<string, unknown>>({});
// 화면 너비에 따라 Y좌표 유지 여부 결정 // 화면 너비에 따라 Y좌표 유지 여부 결정
const [preserveYPosition, setPreserveYPosition] = useState(true); const [preserveYPosition, setPreserveYPosition] = useState(true);
@ -34,7 +34,7 @@ export default function ScreenViewPage() {
const [editModalConfig, setEditModalConfig] = useState<{ const [editModalConfig, setEditModalConfig] = useState<{
screenId?: number; screenId?: number;
modalSize?: "sm" | "md" | "lg" | "xl" | "full"; modalSize?: "sm" | "md" | "lg" | "xl" | "full";
editData?: any; editData?: Record<string, unknown>;
onSave?: () => void; onSave?: () => void;
modalTitle?: string; modalTitle?: string;
modalDescription?: string; modalDescription?: string;
@ -70,11 +70,11 @@ export default function ScreenViewPage() {
setEditModalOpen(true); setEditModalOpen(true);
}; };
// @ts-ignore // @ts-expect-error - CustomEvent type
window.addEventListener("openEditModal", handleOpenEditModal); window.addEventListener("openEditModal", handleOpenEditModal);
return () => { return () => {
// @ts-ignore // @ts-expect-error - CustomEvent type
window.removeEventListener("openEditModal", handleOpenEditModal); window.removeEventListener("openEditModal", handleOpenEditModal);
}; };
}, []); }, []);
@ -96,8 +96,18 @@ export default function ScreenViewPage() {
} catch (layoutError) { } catch (layoutError) {
console.warn("레이아웃 로드 실패, 빈 레이아웃 사용:", layoutError); console.warn("레이아웃 로드 실패, 빈 레이아웃 사용:", layoutError);
setLayout({ setLayout({
screenId,
components: [], components: [],
gridSettings: { columns: 12, gap: 16, padding: 16 }, gridSettings: {
columns: 12,
gap: 16,
padding: 16,
enabled: true,
size: 8,
color: "#e0e0e0",
opacity: 0.5,
snapToGrid: true,
},
}); });
} }
} catch (error) { } catch (error) {
@ -174,6 +184,13 @@ export default function ScreenViewPage() {
containerWidth={window.innerWidth} containerWidth={window.innerWidth}
screenWidth={screenWidth} screenWidth={screenWidth}
preserveYPosition={preserveYPosition} preserveYPosition={preserveYPosition}
isDesignMode={false}
formData={formData}
onFormDataChange={(fieldName: string, value: unknown) => {
console.log("📝 page.tsx formData 업데이트:", fieldName, value);
setFormData((prev) => ({ ...prev, [fieldName]: value }));
}}
screenInfo={{ id: screenId, tableName: screen?.tableName }}
/> />
) : ( ) : (
// 빈 화면일 때 // 빈 화면일 때

View File

@ -10,12 +10,15 @@ import { Breakpoint, BREAKPOINTS } from "@/types/responsive";
import { generateSmartDefaults, ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults"; import { generateSmartDefaults, ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
interface ResponsiveLayoutEngineProps { export interface ResponsiveLayoutEngineProps {
components: ComponentData[]; components: ComponentData[];
breakpoint: Breakpoint; breakpoint: Breakpoint;
containerWidth: number; containerWidth: number;
screenWidth?: number; screenWidth?: number;
preserveYPosition?: boolean; // true: Y좌표 유지 (하이브리드), false: 16px 간격 (반응형) preserveYPosition?: boolean; // true: Y좌표 유지 (하이브리드), false: 16px 간격 (반응형)
formData?: Record<string, unknown>;
onFormDataChange?: (fieldName: string, value: unknown) => void;
screenInfo?: { id: number; tableName?: string };
} }
/** /**
@ -33,6 +36,9 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
containerWidth, containerWidth,
screenWidth = 1920, screenWidth = 1920,
preserveYPosition = false, // 기본값: 반응형 모드 (16px 간격) preserveYPosition = false, // 기본값: 반응형 모드 (16px 간격)
formData,
onFormDataChange,
screenInfo,
}) => { }) => {
// 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화 // 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화
const rows = useMemo(() => { const rows = useMemo(() => {
@ -72,6 +78,18 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
const responsiveComponents = useMemo(() => { const responsiveComponents = useMemo(() => {
const result = sortedRows.flatMap((row, rowIndex) => const result = sortedRows.flatMap((row, rowIndex) =>
row.map((comp, compIndex) => { row.map((comp, compIndex) => {
// 컴포넌트에 gridColumns가 이미 설정되어 있으면 그 값 사용
if ((comp as any).gridColumns !== undefined) {
return {
...comp,
responsiveDisplay: {
gridColumns: (comp as any).gridColumns,
order: compIndex + 1,
hide: false,
},
};
}
// 반응형 설정이 없으면 자동 생성 // 반응형 설정이 없으면 자동 생성
const compWithConfig = ensureResponsiveConfig(comp, screenWidth); const compWithConfig = ensureResponsiveConfig(comp, screenWidth);
@ -140,6 +158,7 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
gap: "16px", gap: "16px",
padding: "0 16px", padding: "0 16px",
marginTop, marginTop,
alignItems: "start", // 각 아이템이 원래 높이 유지
}} }}
> >
{rowComponents.map((comp) => ( {rowComponents.map((comp) => (
@ -148,9 +167,19 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
className="responsive-grid-item" className="responsive-grid-item"
style={{ style={{
gridColumn: `span ${comp.responsiveDisplay?.gridColumns || gridColumns}`, gridColumn: `span ${comp.responsiveDisplay?.gridColumns || gridColumns}`,
height: "auto", // 자동 높이
}} }}
> >
<DynamicComponentRenderer component={comp} isPreview={true} /> <DynamicComponentRenderer
component={comp}
isPreview={true}
isDesignMode={false}
isInteractive={true}
formData={formData}
onFormDataChange={onFormDataChange}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
/>
</div> </div>
))} ))}
</div> </div>

View File

@ -872,7 +872,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
} }
}, []); }, []);
// 화면의 기본 테이블 정보 로드 // 화면의 기본 테이블 정보 로드 (원래대로 복원)
useEffect(() => { useEffect(() => {
const loadScreenTable = async () => { const loadScreenTable = async () => {
const tableName = selectedScreen?.tableName; const tableName = selectedScreen?.tableName;
@ -915,7 +915,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
columns, columns,
}; };
setTables([tableInfo]); setTables([tableInfo]); // 현재 화면의 테이블만 저장 (원래대로)
} catch (error) { } catch (error) {
console.error("화면 테이블 정보 로드 실패:", error); console.error("화면 테이블 정보 로드 실패:", error);
setTables([]); setTables([]);
@ -1996,7 +1996,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
"accordion-basic": 12, // 아코디언 (100%) "accordion-basic": 12, // 아코디언 (100%)
"table-list": 12, // 테이블 리스트 (100%) "table-list": 12, // 테이블 리스트 (100%)
"image-display": 4, // 이미지 표시 (33%) "image-display": 4, // 이미지 표시 (33%)
"split-panel-layout": 12, // 분할 패널 레이아웃 (100%) "split-panel-layout": 6, // 분할 패널 레이아웃 (50%)
// 액션 컴포넌트 (ACTION 카테고리) // 액션 컴포넌트 (ACTION 카테고리)
"button-basic": 1, // 버튼 (8.33%) "button-basic": 1, // 버튼 (8.33%)

View File

@ -95,7 +95,14 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
} }
const newItems = [...items, createEmptyItem()]; const newItems = [...items, createEmptyItem()];
setItems(newItems); setItems(newItems);
onChange?.(newItems); console.log(" RepeaterInput 항목 추가, onChange 호출:", newItems);
// targetTable이 설정된 경우 각 항목에 메타데이터 추가
const dataWithMeta = config.targetTable
? newItems.map((item) => ({ ...item, _targetTable: config.targetTable }))
: newItems;
onChange?.(dataWithMeta);
}; };
// 항목 제거 // 항목 제거
@ -105,7 +112,13 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
} }
const newItems = items.filter((_, i) => i !== index); const newItems = items.filter((_, i) => i !== index);
setItems(newItems); setItems(newItems);
onChange?.(newItems);
// targetTable이 설정된 경우 각 항목에 메타데이터 추가
const dataWithMeta = config.targetTable
? newItems.map((item) => ({ ...item, _targetTable: config.targetTable }))
: newItems;
onChange?.(dataWithMeta);
// 접힌 상태도 업데이트 // 접힌 상태도 업데이트
const newCollapsed = new Set(collapsedItems); const newCollapsed = new Set(collapsedItems);
@ -121,7 +134,19 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
[fieldName]: value, [fieldName]: value,
}; };
setItems(newItems); setItems(newItems);
onChange?.(newItems); console.log("✏️ RepeaterInput 필드 변경, onChange 호출:", {
itemIndex,
fieldName,
value,
newItems,
});
// targetTable이 설정된 경우 각 항목에 메타데이터 추가
const dataWithMeta = config.targetTable
? newItems.map((item) => ({ ...item, _targetTable: config.targetTable }))
: newItems;
onChange?.(dataWithMeta);
}; };
// 접기/펼치기 토글 // 접기/펼치기 토글

View File

@ -18,12 +18,20 @@ export interface RepeaterConfigPanelProps {
config: RepeaterFieldGroupConfig; config: RepeaterFieldGroupConfig;
onChange: (config: RepeaterFieldGroupConfig) => void; onChange: (config: RepeaterFieldGroupConfig) => void;
tableColumns?: ColumnInfo[]; tableColumns?: ColumnInfo[];
allTables?: Array<{ tableName: string; displayName?: string }>; // 전체 테이블 목록
onTableChange?: (tableName: string) => void; // 테이블 변경 시 해당 테이블의 컬럼 로드
} }
/** /**
* *
*/ */
export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({ config, onChange, tableColumns = [] }) => { export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({
config,
onChange,
tableColumns = [],
allTables = [],
onTableChange,
}) => {
const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []); const [localFields, setLocalFields] = useState<RepeaterFieldDefinition[]>(config.fields || []);
const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({}); const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
@ -71,8 +79,79 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({ config
handleFieldsChange(newFields); handleFieldsChange(newFields);
}; };
// 테이블 선택 Combobox 상태
const [tableSelectOpen, setTableSelectOpen] = useState(false);
const [tableSearchValue, setTableSearchValue] = useState("");
// 필터링된 테이블 목록
const filteredTables = useMemo(() => {
if (!tableSearchValue) return allTables;
const searchLower = tableSearchValue.toLowerCase();
return allTables.filter(
(table) =>
table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower),
);
}, [allTables, tableSearchValue]);
// 선택된 테이블 표시명
const selectedTableLabel = useMemo(() => {
if (!config.targetTable) return "테이블을 선택하세요";
const table = allTables.find((t) => t.tableName === config.targetTable);
return table ? table.displayName || table.tableName : config.targetTable;
}, [config.targetTable, allTables]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* 대상 테이블 선택 */}
<div className="space-y-2">
<Label className="text-sm font-semibold"> </Label>
<Popover open={tableSelectOpen} onOpenChange={setTableSelectOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableSelectOpen}
className="w-full justify-between"
>
{selectedTableLabel}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="테이블 검색..." value={tableSearchValue} onValueChange={setTableSearchValue} />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{filteredTables.map((table) => (
<CommandItem
key={table.tableName}
value={table.tableName}
onSelect={(currentValue) => {
handleChange("targetTable", currentValue);
setTableSelectOpen(false);
setTableSearchValue("");
// 선택된 테이블의 컬럼 로드
if (onTableChange) {
onTableChange(currentValue);
}
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.targetTable === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<p className="text-xs text-gray-500"> .</p>
</div>
{/* 필드 정의 */} {/* 필드 정의 */}
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-sm font-semibold"> </Label> <Label className="text-sm font-semibold"> </Label>

View File

@ -77,6 +77,7 @@ export interface DynamicComponentRendererProps {
component: ComponentData; component: ComponentData;
isSelected?: boolean; isSelected?: boolean;
isPreview?: boolean; // 반응형 모드 플래그 isPreview?: boolean; // 반응형 모드 플래그
isDesignMode?: boolean; // 디자인 모드 여부 (false일 때 데이터 로드)
onClick?: (e?: React.MouseEvent) => void; onClick?: (e?: React.MouseEvent) => void;
onDragStart?: (e: React.DragEvent) => void; onDragStart?: (e: React.DragEvent) => void;
onDragEnd?: () => void; onDragEnd?: () => void;
@ -193,8 +194,34 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
autoGeneration: component.autoGeneration, autoGeneration: component.autoGeneration,
hidden: component.hidden, hidden: component.hidden,
isInteractive, isInteractive,
isPreview, // 반응형 모드 플래그
isDesignMode: props.isDesignMode, // 디자인 모드 플래그
}); });
// onChange 핸들러 - 컴포넌트 타입에 따라 다르게 처리
const handleChange = (value: any) => {
console.log("🔄 DynamicComponentRenderer handleChange 호출:", {
componentType,
fieldName,
value,
valueType: typeof value,
isArray: Array.isArray(value),
});
if (onFormDataChange) {
// RepeaterInput 같은 복합 컴포넌트는 전체 데이터를 전달
// 단순 input 컴포넌트는 (fieldName, value) 형태로 전달받음
if (componentType === "repeater-field-group" || componentType === "repeater") {
// fieldName과 함께 전달
console.log("💾 RepeaterInput 데이터 저장:", fieldName, value);
onFormDataChange(fieldName, value);
} else {
// 이미 fieldName이 포함된 경우는 그대로 전달
onFormDataChange(fieldName, value);
}
}
};
// 렌더러 props 구성 // 렌더러 props 구성
const rendererProps = { const rendererProps = {
component, component,
@ -215,7 +242,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
isInteractive, isInteractive,
formData, formData,
onFormDataChange, onFormDataChange,
onChange: onFormDataChange, // onChange도 전달 onChange: handleChange, // 개선된 onChange 핸들러 전달
tableName, tableName,
onRefresh, onRefresh,
onClose, onClose,
@ -237,6 +264,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
refreshKey, refreshKey,
// 반응형 모드 플래그 전달 // 반응형 모드 플래그 전달
isPreview, isPreview,
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
}; };
// 렌더러가 클래스인지 함수인지 확인 // 렌더러가 클래스인지 함수인지 확인

View File

@ -88,18 +88,19 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 삭제 액션 감지 로직 (실제 필드명 사용) // 삭제 액션 감지 로직 (실제 필드명 사용)
const isDeleteAction = () => { const isDeleteAction = () => {
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del']; const deleteKeywords = ["삭제", "delete", "remove", "제거", "del"];
return ( return (
component.componentConfig?.action?.type === 'delete' || component.componentConfig?.action?.type === "delete" ||
component.config?.action?.type === 'delete' || component.config?.action?.type === "delete" ||
component.webTypeConfig?.actionType === 'delete' || component.webTypeConfig?.actionType === "delete" ||
component.text?.toLowerCase().includes('삭제') || component.text?.toLowerCase().includes("삭제") ||
component.text?.toLowerCase().includes('delete') || component.text?.toLowerCase().includes("delete") ||
component.label?.toLowerCase().includes('삭제') || component.label?.toLowerCase().includes("삭제") ||
component.label?.toLowerCase().includes('delete') || component.label?.toLowerCase().includes("delete") ||
deleteKeywords.some(keyword => deleteKeywords.some(
component.config?.buttonText?.toLowerCase().includes(keyword) || (keyword) =>
component.config?.text?.toLowerCase().includes(keyword) component.config?.buttonText?.toLowerCase().includes(keyword) ||
component.config?.text?.toLowerCase().includes(keyword),
) )
); );
}; };
@ -109,9 +110,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
if (isDeleteAction() && !component.style?.labelColor) { if (isDeleteAction() && !component.style?.labelColor) {
// 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정 // 삭제 액션이고 라벨 색상이 설정되지 않은 경우 빨간색으로 자동 설정
if (component.style) { if (component.style) {
component.style.labelColor = '#ef4444'; component.style.labelColor = "#ef4444";
} else { } else {
component.style = { labelColor: '#ef4444' }; component.style = { labelColor: "#ef4444" };
} }
} }
}, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]); }, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]);
@ -125,20 +126,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
const getLabelColor = () => { const getLabelColor = () => {
if (isDeleteAction()) { if (isDeleteAction()) {
return component.style?.labelColor || '#ef4444'; // 빨간색 기본값 (Tailwind red-500) return component.style?.labelColor || "#ef4444"; // 빨간색 기본값 (Tailwind red-500)
} }
return component.style?.labelColor || '#212121'; // 검은색 기본값 (shadcn/ui primary) return component.style?.labelColor || "#212121"; // 검은색 기본값 (shadcn/ui primary)
}; };
const buttonColor = getLabelColor(); const buttonColor = getLabelColor();
// 그라데이션용 어두운 색상 계산 // 그라데이션용 어두운 색상 계산
const getDarkColor = (baseColor: string) => { const getDarkColor = (baseColor: string) => {
const hex = baseColor.replace('#', ''); const hex = baseColor.replace("#", "");
const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40); const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40);
const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40); const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40);
const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40); const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}; };
const buttonDarkColor = getDarkColor(buttonColor); const buttonDarkColor = getDarkColor(buttonColor);
@ -246,6 +247,23 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
currentLoadingToastRef.current = undefined; currentLoadingToastRef.current = undefined;
} }
// 실패한 경우 오류 처리
if (!success) {
console.log("❌ 액션 실패, 오류 토스트 표시");
const errorMessage =
actionConfig.errorMessage ||
(actionConfig.type === "save"
? "저장 중 오류가 발생했습니다."
: actionConfig.type === "delete"
? "삭제 중 오류가 발생했습니다."
: actionConfig.type === "submit"
? "제출 중 오류가 발생했습니다."
: "처리 중 오류가 발생했습니다.");
toast.error(errorMessage);
return;
}
// 성공한 경우에만 성공 토스트 표시
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요) // edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
if (actionConfig.type !== "edit") { if (actionConfig.type !== "edit") {
const successMessage = const successMessage =
@ -268,24 +286,24 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 저장/수정 성공 시 자동 처리 // 저장/수정 성공 시 자동 처리
if (actionConfig.type === "save" || actionConfig.type === "edit") { if (actionConfig.type === "save" || actionConfig.type === "edit") {
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
// 1. 테이블 새로고침 이벤트 먼저 발송 (모달이 닫히기 전에) // 1. 테이블 새로고침 이벤트 먼저 발송 (모달이 닫히기 전에)
console.log("🔄 저장/수정 후 테이블 새로고침 이벤트 발송"); console.log("🔄 저장/수정 후 테이블 새로고침 이벤트 발송");
window.dispatchEvent(new CustomEvent('refreshTable')); window.dispatchEvent(new CustomEvent("refreshTable"));
// 2. 모달 닫기 (약간의 딜레이) // 2. 모달 닫기 (약간의 딜레이)
setTimeout(() => { setTimeout(() => {
// EditModal 내부인지 확인 (isInModal prop 사용) // EditModal 내부인지 확인 (isInModal prop 사용)
const isInEditModal = (props as any).isInModal; const isInEditModal = (props as any).isInModal;
if (isInEditModal) { if (isInEditModal) {
console.log("🚪 EditModal 닫기 이벤트 발송"); console.log("🚪 EditModal 닫기 이벤트 발송");
window.dispatchEvent(new CustomEvent('closeEditModal')); window.dispatchEvent(new CustomEvent("closeEditModal"));
} }
// ScreenModal은 항상 닫기 // ScreenModal은 항상 닫기
console.log("🚪 ScreenModal 닫기 이벤트 발송"); console.log("🚪 ScreenModal 닫기 이벤트 발송");
window.dispatchEvent(new CustomEvent('closeSaveModal')); window.dispatchEvent(new CustomEvent("closeSaveModal"));
}, 100); }, 100);
} }
} }
@ -301,19 +319,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
console.error("❌ 버튼 액션 실행 오류:", error); console.error("❌ 버튼 액션 실행 오류:", error);
// 오류 토스트 표시 // 오류 토스트는 buttonActions.ts에서 이미 표시되므로 여기서는 제거
const errorMessage = // (중복 토스트 방지)
actionConfig.errorMessage ||
(actionConfig.type === "save"
? "저장 중 오류가 발생했습니다."
: actionConfig.type === "delete"
? "삭제 중 오류가 발생했습니다."
: actionConfig.type === "submit"
? "제출 중 오류가 발생했습니다."
: "처리 중 오류가 발생했습니다.");
console.log("💥 오류 토스트 표시:", errorMessage);
toast.error(errorMessage);
} }
}; };
@ -379,7 +386,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
console.log("⚠️ 액션 실행 조건 불만족:", { console.log("⚠️ 액션 실행 조건 불만족:", {
isInteractive, isInteractive,
hasAction: !!processedConfig.action, hasAction: !!processedConfig.action,
"이유": !isInteractive ? "인터랙티브 모드 아님" : "액션 없음", : !isInteractive ? "인터랙티브 모드 아님" : "액션 없음",
}); });
// 액션이 설정되지 않은 경우 기본 onClick 실행 // 액션이 설정되지 않은 경우 기본 onClick 실행
onClick?.(); onClick?.();
@ -479,7 +486,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
maxHeight: "100%", maxHeight: "100%",
border: "none", border: "none",
borderRadius: "8px", borderRadius: "8px",
background: componentConfig.disabled background: componentConfig.disabled
? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)" ? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)"
: `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`, : `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`,
color: componentConfig.disabled ? "#9ca3af" : "white", color: componentConfig.disabled ? "#9ca3af" : "white",
@ -495,9 +502,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
margin: "0", margin: "0",
lineHeight: "1", lineHeight: "1",
minHeight: "36px", minHeight: "36px",
boxShadow: componentConfig.disabled boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
? "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
: `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
// isInteractive 모드에서는 사용자 스타일 우선 적용 // isInteractive 모드에서는 사용자 스타일 우선 적용
...(isInteractive && component.style ? component.style : {}), ...(isInteractive && component.style ? component.style : {}),
}} }}

View File

@ -64,7 +64,7 @@ export class RepeaterFieldGroupRenderer extends AutoRegisteringComponentRenderer
configPanel: RepeaterConfigPanel, configPanel: RepeaterConfigPanel,
defaultSize: { defaultSize: {
width: 600, width: 600,
height: 400, // 여러 항목과 필드를 표시할 수 있도록 높이 설 height: 200, // 기본 높이 조
}, },
defaultConfig: { defaultConfig: {
fields: [], // 빈 배열로 시작 - 사용자가 직접 필드 추가 fields: [], // 빈 배열로 시작 - 사용자가 직접 필드 추가

View File

@ -55,9 +55,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 컴포넌트 스타일 // 컴포넌트 스타일
const componentStyle: React.CSSProperties = isPreview const componentStyle: React.CSSProperties = isPreview
? { ? {
// 반응형 모드: position relative, width/height 100% // 반응형 모드: position relative, 그리드 컨테이너가 제공하는 크기 사용
position: "relative", position: "relative",
width: "100%", // width 제거 - 그리드 컬럼이 결정
height: `${component.style?.height || 600}px`, height: `${component.style?.height || 600}px`,
border: "1px solid #e5e7eb", border: "1px solid #e5e7eb",
} }
@ -257,19 +257,27 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return ( return (
<div <div
ref={containerRef} ref={containerRef}
style={componentStyle} style={
isPreview
? {
position: "relative",
height: `${component.style?.height || 600}px`,
border: "1px solid #e5e7eb",
}
: componentStyle
}
onClick={(e) => { onClick={(e) => {
if (isDesignMode) { if (isDesignMode) {
e.stopPropagation(); e.stopPropagation();
onClick?.(e); onClick?.(e);
} }
}} }}
className="flex overflow-hidden rounded-lg bg-white shadow-sm" className={`flex overflow-hidden rounded-lg bg-white shadow-sm ${isPreview ? "w-full" : ""}`}
> >
{/* 좌측 패널 */} {/* 좌측 패널 */}
<div <div
style={{ width: `${leftWidth}%`, minWidth: `${minLeftWidth}px` }} style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px` }}
className="flex flex-col border-r border-gray-200" className="flex flex-shrink-0 flex-col border-r border-gray-200"
> >
<Card className="flex h-full flex-col border-0 shadow-none"> <Card className="flex h-full flex-col border-0 shadow-none">
<CardHeader className="border-b border-gray-100 pb-3"> <CardHeader className="border-b border-gray-100 pb-3">
@ -404,7 +412,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
)} )}
{/* 우측 패널 */} {/* 우측 패널 */}
<div style={{ width: `${100 - leftWidth}%`, minWidth: `${minRightWidth}px` }} className="flex flex-col"> <div
style={{ width: `${100 - leftWidth}%`, minWidth: isPreview ? "0" : `${minRightWidth}px` }}
className="flex flex-shrink-0 flex-col"
>
<Card className="flex h-full flex-col border-0 shadow-none"> <Card className="flex h-full flex-col border-0 shadow-none">
<CardHeader className="border-b border-gray-100 pb-3"> <CardHeader className="border-b border-gray-100 pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -28,7 +28,7 @@ interface SplitPanelLayoutConfigPanelProps {
export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({
config, config,
onChange, onChange,
tables = [], // 기본값 빈 배열 tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
screenTableName, // 현재 화면의 테이블명 screenTableName, // 현재 화면의 테이블명
}) => { }) => {
const [rightTableOpen, setRightTableOpen] = useState(false); const [rightTableOpen, setRightTableOpen] = useState(false);
@ -36,6 +36,32 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
const [rightColumnOpen, setRightColumnOpen] = useState(false); const [rightColumnOpen, setRightColumnOpen] = useState(false);
const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({}); const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({});
const [loadingColumns, setLoadingColumns] = useState<Record<string, boolean>>({}); const [loadingColumns, setLoadingColumns] = useState<Record<string, boolean>>({});
const [allTables, setAllTables] = useState<any[]>([]); // 조인 모드용 전체 테이블 목록
// 관계 타입
const relationshipType = config.rightPanel?.relation?.type || "detail";
// 조인 모드일 때만 전체 테이블 목록 로드
useEffect(() => {
if (relationshipType === "join") {
const loadAllTables = async () => {
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
console.log("✅ 분할패널 조인 모드: 전체 테이블 목록 로드", response.data.length, "개");
setAllTables(response.data);
}
} catch (error) {
console.error("❌ 전체 테이블 목록 로드 실패:", error);
}
};
loadAllTables();
} else {
// 상세 모드일 때는 기본 테이블만 사용
setAllTables([]);
}
}, [relationshipType]);
// screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정 // screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정
useEffect(() => { useEffect(() => {
@ -155,8 +181,13 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
); );
} }
// 관계 타입에 따라 우측 테이블을 자동으로 설정 // 조인 모드에서 우측 테이블 선택 시 사용할 테이블 목록
const relationshipType = config.rightPanel?.relation?.type || "detail"; const availableRightTables = relationshipType === "join" ? allTables : tables;
console.log("📊 분할패널 테이블 목록 상태:");
console.log(" - relationshipType:", relationshipType);
console.log(" - allTables:", allTables.length, "개");
console.log(" - availableRightTables:", availableRightTables.length, "개");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@ -285,7 +316,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
<CommandInput placeholder="테이블 검색..." /> <CommandInput placeholder="테이블 검색..." />
<CommandEmpty> .</CommandEmpty> <CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto"> <CommandGroup className="max-h-[200px] overflow-auto">
{tables.map((table) => ( {availableRightTables.map((table) => (
<CommandItem <CommandItem
key={table.tableName} key={table.tableName}
value={table.tableName} value={table.tableName}
@ -300,8 +331,8 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
config.rightPanel?.tableName === table.tableName ? "opacity-100" : "opacity-0", config.rightPanel?.tableName === table.tableName ? "opacity-100" : "opacity-0",
)} )}
/> />
{table.tableName} {table.displayName || table.tableName}
<span className="ml-2 text-xs text-gray-500">({table.tableLabel || ""})</span> {table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@ -41,7 +41,7 @@ export const SplitPanelLayoutDefinition = createComponentDefinition({
autoLoad: true, autoLoad: true,
syncSelection: true, syncSelection: true,
} as SplitPanelLayoutConfig, } as SplitPanelLayoutConfig,
defaultSize: { width: 1000, height: 600 }, defaultSize: { width: 800, height: 600 },
configPanel: SplitPanelLayoutConfigPanel, configPanel: SplitPanelLayoutConfigPanel,
icon: "PanelLeftRight", icon: "PanelLeftRight",
tags: ["분할", "마스터", "디테일", "레이아웃"], tags: ["분할", "마스터", "디테일", "레이아웃"],

View File

@ -155,14 +155,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange, onSelectedRowsChange,
onConfigChange, onConfigChange,
refreshKey, refreshKey,
tableName, // 화면의 기본 테이블명 (screenInfo에서 전달)
}) => { }) => {
// 컴포넌트 설정 // 컴포넌트 설정
const tableConfig = { const tableConfig = {
...config, ...config,
...component.config, ...component.config,
...componentConfig, ...componentConfig,
// selectedTable이 없으면 화면의 기본 테이블 사용
selectedTable:
componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName,
} as TableListConfig; } as TableListConfig;
console.log("🔍 TableListComponent 초기화:", {
componentConfigSelectedTable: componentConfig?.selectedTable,
componentConfigSelectedTable2: component.config?.selectedTable,
configSelectedTable: config?.selectedTable,
screenTableName: tableName,
finalSelectedTable: tableConfig.selectedTable,
});
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
const buttonColor = component.style?.labelColor || "#212121"; // 기본 파란색 const buttonColor = component.style?.labelColor || "#212121"; // 기본 파란색
const buttonTextColor = component.config?.buttonTextColor || "#ffffff"; const buttonTextColor = component.config?.buttonTextColor || "#ffffff";
@ -424,20 +436,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} }
}; };
// 디바운싱된 테이블 데이터 가져오기
const fetchTableDataDebounced = useCallback(
debouncedApiCall(
`fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`,
async () => {
return fetchTableDataInternal();
},
200, // 200ms 디바운스
),
[tableConfig.selectedTable, currentPage, localPageSize, searchTerm, sortColumn, sortDirection, searchValues],
);
// 실제 테이블 데이터 가져오기 함수 // 실제 테이블 데이터 가져오기 함수
const fetchTableDataInternal = async () => { const fetchTableDataInternal = useCallback(async () => {
if (!tableConfig.selectedTable) { if (!tableConfig.selectedTable) {
setData([]); setData([]);
return; return;
@ -448,81 +448,54 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
try { try {
// 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회 // 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회
console.log("🔗 Entity 조인 데이터 조회 시작:", tableConfig.selectedTable);
// Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들)
const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || [];
// 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만) // 🎯 조인 탭에서 추가한 컬럼들 추출 (additionalJoinInfo가 있는 컬럼들)
const joinTabColumns = const manualJoinColumns =
tableConfig.columns?.filter( tableConfig.columns?.filter((col) => {
(col) => return col.additionalJoinInfo !== undefined;
!col.isEntityJoin && }) || [];
col.columnName.includes("_") &&
(col.columnName.includes("dept_code_") ||
col.columnName.includes("_dept_code") ||
col.columnName.includes("_company_") ||
col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들
) || [];
console.log( console.log(
"🔍 조인 탭 컬럼들:", "🔗 수동 조인 컬럼 감지:",
joinTabColumns.map((c) => c.columnName), manualJoinColumns.map((c) => ({
columnName: c.columnName,
additionalJoinInfo: c.additionalJoinInfo,
})),
); );
const additionalJoinColumns = [ // 🎯 추가 조인 컬럼 정보 구성
...entityJoinColumns.map((col) => ({ const additionalJoinColumns: Array<{
sourceTable: string;
sourceColumn: string;
joinAlias: string;
referenceTable?: string;
}> = [];
// Entity 조인 컬럼들
entityJoinColumns.forEach((col) => {
additionalJoinColumns.push({
sourceTable: col.entityJoinInfo!.sourceTable, sourceTable: col.entityJoinInfo!.sourceTable,
sourceColumn: col.entityJoinInfo!.sourceColumn, sourceColumn: col.entityJoinInfo!.sourceColumn,
joinAlias: col.entityJoinInfo!.joinAlias, joinAlias: col.entityJoinInfo!.joinAlias,
})), });
// 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) });
...joinTabColumns
.filter((col) => {
// 실제 API 응답에 존재하는 컬럼만 필터링
const validJoinColumns = ["dept_code_name", "dept_name"];
const isValid = validJoinColumns.includes(col.columnName);
if (!isValid) {
console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`);
}
return isValid;
})
.map((col) => {
// 실제 존재하는 조인 컬럼만 처리
let sourceTable = tableConfig.selectedTable;
let sourceColumn = col.columnName;
if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { // 수동 조인 컬럼들 - 저장된 조인 정보 사용
sourceTable = "dept_info"; manualJoinColumns.forEach((col) => {
sourceColumn = "dept_code"; if (col.additionalJoinInfo) {
} additionalJoinColumns.push({
sourceTable: col.additionalJoinInfo.sourceTable,
console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); sourceColumn: col.additionalJoinInfo.sourceColumn,
joinAlias: col.additionalJoinInfo.joinAlias,
return { referenceTable: col.additionalJoinInfo.referenceTable,
sourceTable: sourceTable || tableConfig.selectedTable || "", });
sourceColumn: sourceColumn,
joinAlias: col.columnName,
};
}),
];
// 🎯 화면별 엔티티 표시 설정 생성
const screenEntityConfigs: Record<string, any> = {};
entityJoinColumns.forEach((col) => {
if (col.entityDisplayConfig) {
const sourceColumn = col.entityJoinInfo!.sourceColumn;
screenEntityConfigs[sourceColumn] = {
displayColumns: col.entityDisplayConfig.displayColumns,
separator: col.entityDisplayConfig.separator || " - ",
};
} }
}); });
console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); console.log("🔗 최종 추가 조인 컬럼:", additionalJoinColumns);
console.log("🔗 조인 탭 컬럼:", joinTabColumns);
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
// console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs);
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
page: currentPage, page: currentPage,
@ -591,7 +564,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortOrder: sortDirection, sortOrder: sortDirection,
enableEntityJoin: true, // 🎯 Entity 조인 활성화 enableEntityJoin: true, // 🎯 Entity 조인 활성화
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정
}); });
if (result) { if (result) {
@ -661,16 +633,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const actualApiColumns = Object.keys(result.data[0]); const actualApiColumns = Object.keys(result.data[0]);
console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns);
// 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) // 🎯 조인 컬럼 매핑 테이블 - 동적 생성
// 실제 API 응답에 존재하는 컬럼만 매핑 // API 응답에 실제로 존재하는 컬럼과 사용자 설정 컬럼을 비교하여 자동 매핑
const newJoinColumnMapping: Record<string, string> = { const newJoinColumnMapping: Record<string, string> = {};
dept_code_dept_code: "dept_code", // user_info.dept_code
dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) processedColumns.forEach((col) => {
dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) // API 응답에 정확히 일치하는 컬럼이 있으면 그대로 사용
dept_code_name: "dept_code_name", // dept_info.dept_name if (actualApiColumns.includes(col.columnName)) {
dept_name: "dept_name", // dept_info.dept_name newJoinColumnMapping[col.columnName] = col.columnName;
status: "status", // user_info.status }
}; });
// 🎯 조인 컬럼 매핑 상태 업데이트 // 🎯 조인 컬럼 매핑 상태 업데이트
setJoinColumnMapping(newJoinColumnMapping); setJoinColumnMapping(newJoinColumnMapping);
@ -795,7 +767,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [
tableConfig.selectedTable,
tableConfig.columns,
currentPage,
localPageSize,
searchTerm,
sortColumn,
sortDirection,
searchValues,
]);
// 디바운싱된 테이블 데이터 가져오기
const fetchTableDataDebounced = useCallback(
debouncedApiCall(
`fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`,
async () => {
return fetchTableDataInternal();
},
200, // 200ms 디바운스
),
[
tableConfig.selectedTable,
currentPage,
localPageSize,
searchTerm,
sortColumn,
sortDirection,
searchValues,
fetchTableDataInternal,
],
);
// 페이지 변경 // 페이지 변경
const handlePageChange = (newPage: number) => { const handlePageChange = (newPage: number) => {
@ -947,12 +949,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} }
}, [columnLabels]); }, [columnLabels]);
// 🎯 컬럼 개수와 컬럼명을 문자열로 변환하여 의존성 추적
const columnsKey = useMemo(() => {
if (!tableConfig.columns) return "";
return tableConfig.columns.map((col) => col.columnName).join(",");
}, [tableConfig.columns]);
useEffect(() => { useEffect(() => {
if (tableConfig.autoLoad && !isDesignMode) { // autoLoad가 undefined거나 true일 때 자동 로드 (기본값: true)
fetchTableDataDebounced(); const shouldAutoLoad = tableConfig.autoLoad !== false;
console.log("🔍 TableList 데이터 로드 조건 체크:", {
shouldAutoLoad,
isDesignMode,
selectedTable: tableConfig.selectedTable,
autoLoadSetting: tableConfig.autoLoad,
willLoad: shouldAutoLoad && !isDesignMode,
});
if (shouldAutoLoad && !isDesignMode) {
console.log("✅ 테이블 데이터 로드 시작:", tableConfig.selectedTable);
fetchTableDataInternal();
} else {
console.warn("⚠️ 테이블 데이터 로드 차단:", {
reason: !shouldAutoLoad ? "autoLoad=false" : "isDesignMode=true",
shouldAutoLoad,
isDesignMode,
});
} }
}, [ }, [
tableConfig.selectedTable, tableConfig.selectedTable,
columnsKey, // 🎯 컬럼이 추가/변경될 때 데이터 다시 로드 (문자열 비교)
localPageSize, localPageSize,
currentPage, currentPage,
searchTerm, searchTerm,
@ -960,6 +987,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortDirection, sortDirection,
columnLabels, columnLabels,
searchValues, searchValues,
fetchTableDataInternal, // 의존성 배열에 추가
]); ]);
// refreshKey 변경 시 테이블 데이터 새로고침 // refreshKey 변경 시 테이블 데이터 새로고침
@ -992,7 +1020,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}; };
window.addEventListener("refreshTable", handleRefreshTable); window.addEventListener("refreshTable", handleRefreshTable);
return () => { return () => {
window.removeEventListener("refreshTable", handleRefreshTable); window.removeEventListener("refreshTable", handleRefreshTable);
}; };
@ -1314,35 +1342,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onDragEnd, onDragEnd,
}; };
// 디자인 모드에서의 플레이스홀더 // 플레이스홀더 제거 - 디자인 모드에서도 바로 테이블 표시
if (isDesignMode && !tableConfig.selectedTable) {
return (
<div style={componentStyle} className={className} {...domProps}>
<div className="flex h-full items-center justify-center rounded-2xl border-2 border-dashed border-blue-200 bg-gradient-to-br from-blue-50/30 to-indigo-50/20">
<div className="p-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-100 to-indigo-100 shadow-sm">
<TableIcon className="h-8 w-8 text-blue-600" />
</div>
<div className="mb-2 text-lg font-semibold text-slate-700"> </div>
<div className="rounded-full bg-white/60 px-4 py-2 text-sm text-slate-500">
</div>
</div>
</div>
</div>
);
}
return ( return (
<div <div
style={{ ...componentStyle, zIndex: 10 }} // 🎯 componentStyle + z-index 추가 style={{ ...componentStyle, zIndex: 10 }} // 🎯 componentStyle + z-index 추가
className={cn( className={cn(
"relative overflow-hidden", "relative overflow-hidden",
"bg-white border border-gray-200/60", "border border-gray-200/60 bg-white",
"rounded-2xl shadow-sm", "rounded-2xl shadow-sm",
"backdrop-blur-sm", "backdrop-blur-sm",
"transition-all duration-300 ease-out", "transition-all duration-300 ease-out",
isSelected && "ring-2 ring-blue-500/20 shadow-lg shadow-blue-500/10", isSelected && "shadow-lg ring-2 shadow-blue-500/10 ring-blue-500/20",
className, className,
)} )}
{...domProps} {...domProps}
@ -1359,7 +1370,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
> >
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{(tableConfig.title || tableLabel) && ( {(tableConfig.title || tableLabel) && (
<h3 className="text-xl font-semibold text-gray-800 tracking-tight">{tableConfig.title || tableLabel}</h3> <h3 className="text-xl font-semibold tracking-tight text-gray-800">{tableConfig.title || tableLabel}</h3>
)} )}
</div> </div>
@ -1377,16 +1388,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm" size="sm"
onClick={handleRefresh} onClick={handleRefresh}
disabled={loading} disabled={loading}
className="group relative rounded-xl border-gray-200/60 bg-white/80 backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-200 hover:bg-gray-50/80" className="group relative rounded-xl border-gray-200/60 bg-white/80 shadow-sm backdrop-blur-sm transition-all duration-200 hover:bg-gray-50/80 hover:shadow-md"
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="relative"> <div className="relative">
<RefreshCw className={cn("h-4 w-4 text-gray-600", loading && "animate-spin")} /> <RefreshCw className={cn("h-4 w-4 text-gray-600", loading && "animate-spin")} />
{loading && <div className="absolute -inset-1 animate-pulse rounded-full bg-blue-100/40"></div>} {loading && <div className="absolute -inset-1 animate-pulse rounded-full bg-blue-100/40"></div>}
</div> </div>
<span className="text-sm font-medium text-gray-700"> <span className="text-sm font-medium text-gray-700">{loading ? "새로고침 중..." : "새로고침"}</span>
{loading ? "새로고침 중..." : "새로고침"}
</span>
</div> </div>
</Button> </Button>
</div> </div>
@ -1424,7 +1433,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{/* 테이블 컨텐츠 */} {/* 테이블 컨텐츠 */}
<div <div
className={`w-full overflow-auto flex-1`} className={`w-full flex-1 overflow-auto`}
style={{ style={{
width: "100%", width: "100%",
maxWidth: "100%", maxWidth: "100%",
@ -1622,7 +1631,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
<TableCell <TableCell
key={column.columnName} key={column.columnName}
className={cn( className={cn(
"h-12 px-6 py-4 align-middle text-sm transition-all duration-200 text-gray-600", "h-12 px-6 py-4 align-middle text-sm text-gray-600 transition-all duration-200",
`text-${column.align}`, `text-${column.align}`,
)} )}
style={{ style={{
@ -1687,7 +1696,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
</div> </div>
{/* 푸터/페이지네이션 */} {/* 푸터/페이지네이션 */}
{tableConfig.showFooter && tableConfig.pagination?.enabled && ( {/* showFooter와 pagination.enabled의 기본값은 true */}
{tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
<div <div
className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6" className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6"
style={{ style={{
@ -1749,7 +1759,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 데이터는 useEffect에서 자동으로 다시 로드됨 // 데이터는 useEffect에서 자동으로 다시 로드됨
}} }}
className="rounded-xl border border-gray-200/60 bg-white/80 backdrop-blur-sm px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md" className="rounded-xl border border-gray-200/60 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md"
> >
{(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => ( {(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (
<option key={size} value={size}> <option key={size} value={size}>
@ -1760,13 +1770,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
)} )}
{/* 페이지네이션 버튼 */} {/* 페이지네이션 버튼 */}
<div className="flex items-center space-x-2 rounded-xl border border-gray-200/60 bg-white/80 backdrop-blur-sm p-1 shadow-sm"> <div className="flex items-center space-x-2 rounded-xl border border-gray-200/60 bg-white/80 p-1 shadow-sm backdrop-blur-sm">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handlePageChange(1)} onClick={() => handlePageChange(1)}
disabled={currentPage === 1} disabled={currentPage === 1}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200" className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
> >
<ChevronsLeft className="h-4 w-4" /> <ChevronsLeft className="h-4 w-4" />
</Button> </Button>
@ -1775,7 +1785,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm" size="sm"
onClick={() => handlePageChange(currentPage - 1)} onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1} disabled={currentPage === 1}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200" className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</Button> </Button>
@ -1791,7 +1801,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm" size="sm"
onClick={() => handlePageChange(currentPage + 1)} onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200" className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
> >
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Button> </Button>
@ -1800,7 +1810,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
size="sm" size="sm"
onClick={() => handlePageChange(totalPages)} onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="h-8 w-8 p-0 rounded-lg border-gray-200/60 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50 transition-all duration-200" className="h-8 w-8 rounded-lg border-gray-200/60 p-0 transition-all duration-200 hover:border-gray-300/60 hover:bg-gray-50/80 hover:text-gray-700 disabled:opacity-50"
> >
<ChevronsRight className="h-4 w-4" /> <ChevronsRight className="h-4 w-4" />
</Button> </Button>

View File

@ -97,13 +97,20 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
> >
>({}); >({});
// 화면 테이블명이 있으면 자동으로 설정 // 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
useEffect(() => { useEffect(() => {
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) { if (screenTableName && !config.selectedTable) {
console.log("🔄 화면 테이블명 자동 설정:", screenTableName); // 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
onChange({ selectedTable: screenTableName }); const updatedConfig = {
...config,
selectedTable: screenTableName,
// 컬럼이 있으면 유지, 없으면 빈 배열
columns: config.columns || [],
};
onChange(updatedConfig);
} }
}, [screenTableName, config.selectedTable, onChange]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
// 테이블 목록 가져오기 // 테이블 목록 가져오기
useEffect(() => { useEffect(() => {
@ -137,25 +144,32 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
screenTableName, screenTableName,
); );
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우에만 컬럼 목록 표시 // 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || (screenTableName && config.columns && config.columns.length > 0); const shouldShowColumns = config.selectedTable || screenTableName;
if (!shouldShowColumns) { if (!shouldShowColumns) {
console.log("🔧 컬럼 목록 숨김 - 명시적 테이블 선택 또는 설정된 컬럼이 없음"); console.log("🔧 컬럼 목록 숨김 - 테이블이 선택되지 않음");
setAvailableColumns([]); setAvailableColumns([]);
return; return;
} }
// tableColumns prop을 우선 사용하되, 컴포넌트가 명시적으로 설정되었을 때만 // tableColumns prop을 우선 사용
if (tableColumns && tableColumns.length > 0 && (config.selectedTable || config.columns?.length > 0)) { if (tableColumns && tableColumns.length > 0) {
console.log("🔧 tableColumns prop 사용:", tableColumns);
const mappedColumns = tableColumns.map((column: any) => ({ const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name, columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text", dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name, label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
})); }));
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
setAvailableColumns(mappedColumns); setAvailableColumns(mappedColumns);
// selectedTable이 없으면 screenTableName으로 설정
if (!config.selectedTable && screenTableName) {
onChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
} else if (config.selectedTable || screenTableName) { } else if (config.selectedTable || screenTableName) {
// API에서 컬럼 정보 가져오기 // API에서 컬럼 정보 가져오기
const fetchColumns = async () => { const fetchColumns = async () => {
@ -190,7 +204,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
} else { } else {
setAvailableColumns([]); setAvailableColumns([]);
} }
}, [config.selectedTable, screenTableName, tableColumns, config.columns]); }, [config.selectedTable, screenTableName, tableColumns]);
// Entity 조인 컬럼 정보 가져오기 // Entity 조인 컬럼 정보 가져오기
useEffect(() => { useEffect(() => {
@ -235,7 +249,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// hasOnChange: !!onChange, // hasOnChange: !!onChange,
// onChangeType: typeof onChange, // onChangeType: typeof onChange,
// }); // });
const parentValue = config[parentKey] as any; const parentValue = config[parentKey] as any;
const newConfig = { const newConfig = {
[parentKey]: { [parentKey]: {
@ -243,7 +257,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
[childKey]: value, [childKey]: value,
}, },
}; };
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig); // console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig); onChange(newConfig);
}; };
@ -275,8 +289,30 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리) // 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => { const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
console.log("🔗 조인 컬럼 추가 요청:", {
joinColumn,
joinAlias: joinColumn.joinAlias,
columnLabel: joinColumn.columnLabel,
tableName: joinColumn.tableName,
columnName: joinColumn.columnName,
});
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias); const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) return; if (existingColumn) {
console.warn("⚠️ 이미 존재하는 컬럼:", joinColumn.joinAlias);
return;
}
// 🎯 joinTables에서 sourceColumn 찾기
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
console.log("🔍 조인 정보 추출:", {
tableName: joinColumn.tableName,
foundJoinTable: !!joinTableInfo,
sourceColumn,
joinConfig: joinTableInfo?.joinConfig,
});
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false) // 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
const newColumn: ColumnConfig = { const newColumn: ColumnConfig = {
@ -289,10 +325,21 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
format: "text", format: "text",
order: config.columns?.length || 0, order: config.columns?.length || 0,
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님 isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
// 🎯 추가 조인 정보 저장
additionalJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "", // 기준 테이블 (예: user_info)
sourceColumn: sourceColumn, // 기준 컬럼 (예: dept_code) - joinTables에서 추출
referenceTable: joinColumn.tableName, // 참조 테이블 (예: dept_info)
joinAlias: joinColumn.joinAlias, // 조인 별칭 (예: dept_code_company_name)
},
}; };
handleChange("columns", [...(config.columns || []), newColumn]); handleChange("columns", [...(config.columns || []), newColumn]);
console.log("🔗 조인 컬럼 추가됨 (일반 컬럼으로 처리):", newColumn); console.log("✅ 조인 컬럼 추가 완료:", {
columnName: newColumn.columnName,
displayName: newColumn.displayName,
totalColumns: (config.columns?.length || 0) + 1,
});
}; };
// 컬럼 제거 // 컬럼 제거
@ -309,17 +356,31 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
}; };
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정 // 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
const prevColumnsLengthRef = React.useRef<number>(0);
useEffect(() => { useEffect(() => {
const currentLength = config.columns?.length || 0;
const prevLength = prevColumnsLengthRef.current;
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", { console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
hasColumns: !!config.columns, hasColumns: !!config.columns,
columnsCount: config.columns?.length || 0, columnsCount: currentLength,
prevColumnsCount: prevLength,
hasTableColumns: !!tableColumns, hasTableColumns: !!tableColumns,
tableColumnsCount: tableColumns?.length || 0, tableColumnsCount: tableColumns?.length || 0,
selectedTable: config.selectedTable, selectedTable: config.selectedTable,
}); });
if (!config.columns || !tableColumns) { if (!config.columns || !tableColumns || config.columns.length === 0) {
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵"); console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
prevColumnsLengthRef.current = currentLength;
return;
}
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
if (currentLength === prevLength && prevLength > 0) {
console.log(" 컬럼 개수 변경 없음, 엔티티 감지 스킵");
return; return;
} }
@ -352,14 +413,14 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
...column, ...column,
isEntityJoin: true, isEntityJoin: true,
entityJoinInfo: { entityJoinInfo: {
sourceTable: config.selectedTable || "", sourceTable: config.selectedTable || screenTableName || "",
sourceColumn: column.columnName, sourceColumn: column.columnName,
joinAlias: column.columnName, joinAlias: column.columnName,
}, },
entityDisplayConfig: { entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화 displayColumns: [], // 빈 배열로 초기화
separator: " - ", separator: " - ",
sourceTable: config.selectedTable || "", sourceTable: config.selectedTable || screenTableName || "",
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "", joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
}, },
}; };
@ -377,7 +438,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
} else { } else {
console.log(" 엔티티 컬럼 변경사항 없음"); console.log(" 엔티티 컬럼 변경사항 없음");
} }
}, [config.columns, tableColumns, config.selectedTable]);
// 현재 컬럼 개수를 저장
prevColumnsLengthRef.current = currentLength;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.columns?.length, tableColumns, config.selectedTable]); // 컬럼 개수 변경 시에만 실행
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드 // 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => { const loadEntityDisplayConfig = async (column: ColumnConfig) => {
@ -400,6 +465,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
// entityDisplayConfig가 없으면 초기화 // entityDisplayConfig가 없으면 초기화
if (!column.entityDisplayConfig) { if (!column.entityDisplayConfig) {
console.log("🔧 entityDisplayConfig 초기화:", column.columnName); console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
// sourceTable을 결정: entityJoinInfo -> config.selectedTable -> screenTableName 순서
const initialSourceTable = column.entityJoinInfo?.sourceTable || config.selectedTable || screenTableName;
if (!initialSourceTable) {
console.warn("⚠️ sourceTable을 결정할 수 없어서 초기화 실패:", column.columnName);
return;
}
const updatedColumns = config.columns?.map((col) => { const updatedColumns = config.columns?.map((col) => {
if (col.columnName === column.columnName) { if (col.columnName === column.columnName) {
return { return {
@ -407,7 +481,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
entityDisplayConfig: { entityDisplayConfig: {
displayColumns: [], displayColumns: [],
separator: " - ", separator: " - ",
sourceTable: config.selectedTable || "", sourceTable: initialSourceTable,
joinTable: "", joinTable: "",
}, },
}; };
@ -430,15 +504,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig); console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig)); console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기 // sourceTable 결정 우선순위:
let sourceTable = column.entityDisplayConfig.sourceTable; // 1. entityDisplayConfig.sourceTable
// 2. entityJoinInfo.sourceTable
// 3. config.selectedTable
// 4. screenTableName
let sourceTable =
column.entityDisplayConfig.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
let joinTable = column.entityDisplayConfig.joinTable; let joinTable = column.entityDisplayConfig.joinTable;
if (!sourceTable && column.entityJoinInfo) { // sourceTable이 여전히 비어있으면 에러
sourceTable = column.entityJoinInfo.sourceTable; if (!sourceTable) {
console.error("❌ sourceTable이 비어있어서 처리 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
return;
} }
if (!joinTable) { console.log("✅ sourceTable 결정됨:", sourceTable);
if (!joinTable && sourceTable) {
// joinTable이 없으면 tableTypeApi로 조회해서 설정 // joinTable이 없으면 tableTypeApi로 조회해서 설정
try { try {
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable); console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
@ -464,10 +557,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
if (updatedColumns) { if (updatedColumns) {
handleChange("columns", updatedColumns); handleChange("columns", updatedColumns);
} }
} else {
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
} }
} catch (error) { } catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error); console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
console.log("❌ 조회 실패 상세:", { sourceTable, columnName: column.columnName });
} }
} else if (!joinTable) {
console.warn("⚠️ sourceTable이 없어서 joinTable 조회 불가:", column.columnName);
} }
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable }); console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
@ -789,15 +887,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<div className="space-y-4 border-t pt-4"> <div className="space-y-4 border-t pt-4">
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-sm font-medium"> </Label> <Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cards-per-row"> </Label> <Label htmlFor="cards-per-row"> </Label>
<Select <Select
value={config.cardConfig?.cardsPerRow?.toString() || "3"} value={config.cardConfig?.cardsPerRow?.toString() || "3"}
onValueChange={(value) => onValueChange={(value) => handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))}
handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@ -819,9 +915,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
id="card-spacing" id="card-spacing"
type="number" type="number"
value={config.cardConfig?.cardSpacing || 16} value={config.cardConfig?.cardSpacing || 16}
onChange={(e) => onChange={(e) => handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))}
handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))
}
min="0" min="0"
max="50" max="50"
/> />
@ -830,15 +924,13 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<div className="space-y-3"> <div className="space-y-3">
<Label className="text-sm font-medium"> </Label> <Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-1 gap-3"> <div className="grid grid-cols-1 gap-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="id-column">ID ( )</Label> <Label htmlFor="id-column">ID ( )</Label>
<Select <Select
value={config.cardConfig?.idColumn || ""} value={config.cardConfig?.idColumn || ""}
onValueChange={(value) => onValueChange={(value) => handleNestedChange("cardConfig", "idColumn", value)}
handleNestedChange("cardConfig", "idColumn", value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="ID 컬럼 선택" /> <SelectValue placeholder="ID 컬럼 선택" />
@ -857,9 +949,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="title-column"> ( )</Label> <Label htmlFor="title-column"> ( )</Label>
<Select <Select
value={config.cardConfig?.titleColumn || ""} value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) => onValueChange={(value) => handleNestedChange("cardConfig", "titleColumn", value)}
handleNestedChange("cardConfig", "titleColumn", value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="제목 컬럼 선택" /> <SelectValue placeholder="제목 컬럼 선택" />
@ -878,9 +968,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="subtitle-column"> ( )</Label> <Label htmlFor="subtitle-column"> ( )</Label>
<Select <Select
value={config.cardConfig?.subtitleColumn || ""} value={config.cardConfig?.subtitleColumn || ""}
onValueChange={(value) => onValueChange={(value) => handleNestedChange("cardConfig", "subtitleColumn", value)}
handleNestedChange("cardConfig", "subtitleColumn", value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" /> <SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
@ -900,9 +988,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Label htmlFor="description-column"> </Label> <Label htmlFor="description-column"> </Label>
<Select <Select
value={config.cardConfig?.descriptionColumn || ""} value={config.cardConfig?.descriptionColumn || ""}
onValueChange={(value) => onValueChange={(value) => handleNestedChange("cardConfig", "descriptionColumn", value)}
handleNestedChange("cardConfig", "descriptionColumn", value)
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" /> <SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
@ -924,7 +1010,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Checkbox <Checkbox
id="show-card-actions" id="show-card-actions"
checked={config.cardConfig?.showActions !== false} checked={config.cardConfig?.showActions !== false}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
handleNestedChange("cardConfig", "showActions", checked as boolean) handleNestedChange("cardConfig", "showActions", checked as boolean)
} }
/> />
@ -1270,7 +1356,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => loadEntityDisplayConfig(column)} onClick={() => {
// sourceTable 정보가 있는지 확인
const hasSourceTable =
column.entityDisplayConfig?.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
if (!hasSourceTable) {
console.error("❌ sourceTable 정보를 찾을 수 없어서 컬럼 로드 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
alert("컬럼 정보를 로드할 수 없습니다. 테이블 정보가 없습니다.");
return;
}
loadEntityDisplayConfig(column);
}}
disabled={
!column.entityDisplayConfig?.sourceTable &&
!column.entityJoinInfo?.sourceTable &&
!config.selectedTable &&
!screenTableName
}
className="h-6 text-xs" className="h-6 text-xs"
> >
<Plus className="mr-1 h-3 w-3" /> <Plus className="mr-1 h-3 w-3" />

View File

@ -72,21 +72,29 @@ export interface ColumnConfig {
// 새로운 기능들 // 새로운 기능들
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김) hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
autoGeneration?: AutoGenerationConfig; // 자동생성 설정 autoGeneration?: AutoGenerationConfig; // 자동생성 설정
// 🎯 추가 조인 컬럼 정보 (조인 탭에서 추가한 컬럼들)
additionalJoinInfo?: {
sourceTable: string; // 원본 테이블
sourceColumn: string; // 원본 컬럼 (예: dept_code)
referenceTable?: string; // 참조 테이블 (예: dept_info)
joinAlias: string; // 조인 별칭 (예: dept_code_company_name)
};
} }
/** /**
* *
*/ */
export interface CardDisplayConfig { export interface CardDisplayConfig {
idColumn: string; // ID 컬럼 (사번 등) idColumn: string; // ID 컬럼 (사번 등)
titleColumn: string; // 제목 컬럼 (이름 등) titleColumn: string; // 제목 컬럼 (이름 등)
subtitleColumn?: string; // 부제목 컬럼 (부서 등) subtitleColumn?: string; // 부제목 컬럼 (부서 등)
descriptionColumn?: string; // 설명 컬럼 descriptionColumn?: string; // 설명 컬럼
imageColumn?: string; // 이미지 컬럼 imageColumn?: string; // 이미지 컬럼
cardsPerRow: number; // 한 행당 카드 수 (기본: 3) cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
cardSpacing: number; // 카드 간격 (기본: 16px) cardSpacing: number; // 카드 간격 (기본: 16px)
showActions: boolean; // 액션 버튼 표시 여부 showActions: boolean; // 액션 버튼 표시 여부
cardHeight?: number; // 카드 높이 (기본: auto) cardHeight?: number; // 카드 높이 (기본: auto)
} }
/** /**
@ -163,11 +171,11 @@ export interface CheckboxConfig {
*/ */
export interface TableListConfig extends ComponentConfig { export interface TableListConfig extends ComponentConfig {
// 표시 모드 설정 // 표시 모드 설정
displayMode?: "table" | "card"; // 기본: "table" displayMode?: "table" | "card"; // 기본: "table"
// 카드 디스플레이 설정 (displayMode가 "card"일 때 사용) // 카드 디스플레이 설정 (displayMode가 "card"일 때 사용)
cardConfig?: CardDisplayConfig; cardConfig?: CardDisplayConfig;
// 테이블 기본 설정 // 테이블 기본 설정
selectedTable?: string; selectedTable?: string;
tableName?: string; tableName?: string;

View File

@ -210,6 +210,12 @@ export class EnhancedFormService {
* ( ) * ( )
*/ */
private async getTableColumns(tableName: string): Promise<ColumnInfo[]> { private async getTableColumns(tableName: string): Promise<ColumnInfo[]> {
// tableName이 비어있으면 빈 배열 반환
if (!tableName || tableName.trim() === "") {
console.warn("⚠️ getTableColumns: tableName이 비어있음");
return [];
}
// 캐시 확인 // 캐시 확인
const cached = this.columnCache.get(tableName); const cached = this.columnCache.get(tableName);
if (cached) { if (cached) {

View File

@ -119,9 +119,12 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
}) => { }) => {
console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`); console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`);
// 모든 useState를 최상단에 선언 (Hooks 규칙)
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null); const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
React.useEffect(() => { React.useEffect(() => {
let mounted = true; let mounted = true;
@ -155,6 +158,29 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
}; };
}, [componentId]); }, [componentId]);
// tableColumns가 변경되면 selectedTableColumns도 업데이트
React.useEffect(() => {
setSelectedTableColumns(tableColumns);
}, [tableColumns]);
// RepeaterConfigPanel인 경우에만 전체 테이블 목록 로드
React.useEffect(() => {
if (componentId === "repeater-field-group") {
const loadAllTables = async () => {
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setAllTablesList(response.data);
}
} catch (error) {
console.error("전체 테이블 목록 로드 실패:", error);
}
};
loadAllTables();
}
}, [componentId]);
if (loading) { if (loading) {
return ( return (
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4"> <div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-4">
@ -199,16 +225,63 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns, tableColumns: Array.isArray(tableColumns) ? tableColumns.length : tableColumns,
tables: Array.isArray(tables) ? tables.length : tables, tables: Array.isArray(tables) ? tables.length : tables,
tablesType: typeof tables, tablesType: typeof tables,
tablesDetail: tables, // 전체 테이블 목록 확인
}); });
// 테이블 변경 핸들러 - 선택된 테이블의 컬럼을 동적으로 로드
const handleTableChange = async (tableName: string) => {
console.log("🔄 테이블 변경:", tableName);
try {
// 먼저 tables에서 찾아보기 (이미 컬럼이 있는 경우)
const existingTable = tables?.find((t) => t.tableName === tableName);
if (existingTable && existingTable.columns && existingTable.columns.length > 0) {
console.log("✅ 캐시된 테이블 컬럼 사용:", existingTable.columns.length, "개");
setSelectedTableColumns(existingTable.columns);
return;
}
// 컬럼이 없으면 tableTypeApi로 조회 (ScreenDesigner와 동일한 방식)
console.log("🔍 테이블 컬럼 API 조회:", tableName);
const { tableTypeApi } = await import("@/lib/api/screen");
const columnsResponse = await tableTypeApi.getColumns(tableName);
console.log("🔍 컬럼 응답 데이터:", columnsResponse);
const columns = (columnsResponse || []).map((col: any) => ({
tableName: col.tableName || tableName,
columnName: col.columnName || col.column_name,
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
dataType: col.dataType || col.data_type || col.dbType,
webType: col.webType || col.web_type,
input_type: col.inputType || col.input_type,
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
isNullable: col.isNullable || col.is_nullable,
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
columnDefault: col.columnDefault || col.column_default,
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
codeCategory: col.codeCategory || col.code_category,
codeValue: col.codeValue || col.code_value,
}));
console.log("✅ 테이블 컬럼 로드 성공:", columns.length, "개");
setSelectedTableColumns(columns);
} catch (error) {
console.error("❌ 테이블 변경 오류:", error);
// 오류 발생 시 빈 배열
setSelectedTableColumns([]);
}
};
return ( return (
<ConfigPanelComponent <ConfigPanelComponent
config={config} config={config}
onChange={onChange} onChange={onChange}
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
screenTableName={screenTableName} screenTableName={screenTableName}
tableColumns={tableColumns} tableColumns={selectedTableColumns} // 동적으로 변경되는 컬럼 전달
tables={tables} // 전체 테이블 목록 전달 tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
/> />
); );
}; };

View File

@ -18,11 +18,12 @@ export function generateSmartDefaults(
screenWidth: number = 1920, screenWidth: number = 1920,
rowComponentCount: number = 1, // 같은 행에 있는 컴포넌트 개수 rowComponentCount: number = 1, // 같은 행에 있는 컴포넌트 개수
): ResponsiveComponentConfig["responsive"] { ): ResponsiveComponentConfig["responsive"] {
// 특정 컴포넌트는 항상 전체 너비 (split-panel-layout, datatable 등) // 특정 컴포넌트는 항상 전체 너비 (datatable, table-list 등)
const fullWidthComponents = ["split-panel-layout", "datatable", "data-table"]; const fullWidthComponents = ["datatable", "data-table", "table-list", "repeater-field-group"];
const componentId = (component as any).componentId || (component as any).id;
const componentType = (component as any).componentType || component.type; const componentType = (component as any).componentType || component.type;
if (fullWidthComponents.includes(componentType)) { if (fullWidthComponents.includes(componentId) || fullWidthComponents.includes(componentType)) {
return { return {
desktop: { desktop: {
gridColumns: 12, // 전체 너비 gridColumns: 12, // 전체 너비

View File

@ -29,6 +29,7 @@ export interface RepeaterFieldDefinition {
*/ */
export interface RepeaterFieldGroupConfig { export interface RepeaterFieldGroupConfig {
fields: RepeaterFieldDefinition[]; // 반복될 필드 정의 fields: RepeaterFieldDefinition[]; // 반복될 필드 정의
targetTable?: string; // 저장할 대상 테이블 (미지정 시 메인 화면 테이블)
minItems?: number; // 최소 항목 수 minItems?: number; // 최소 항목 수
maxItems?: number; // 최대 항목 수 maxItems?: number; // 최대 항목 수
addButtonText?: string; // 추가 버튼 텍스트 addButtonText?: string; // 추가 버튼 텍스트