반응형 및 테이블 리스트 컴포넌트 오류 수정
This commit is contained in:
parent
54e9f45823
commit
2a8081a253
|
|
@ -515,6 +515,7 @@ export class DashboardController {
|
|||
});
|
||||
|
||||
// 외부 API 호출
|
||||
// @ts-ignore - node-fetch dynamic import
|
||||
const fetch = (await import("node-fetch")).default;
|
||||
const response = await fetch(urlObj.toString(), {
|
||||
method: method.toUpperCase(),
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
if (!tableColumns.includes(key)) {
|
||||
|
|
@ -305,6 +353,9 @@ export class DynamicFormService {
|
|||
}
|
||||
});
|
||||
|
||||
// RepeaterInput 데이터 처리 로직은 메인 저장 후에 처리
|
||||
// (각 Repeater가 다른 테이블에 저장될 수 있으므로)
|
||||
|
||||
console.log("🎯 실제 테이블에 삽입할 데이터:", {
|
||||
tableName,
|
||||
dataToInsert,
|
||||
|
|
@ -388,6 +439,111 @@ export class DynamicFormService {
|
|||
// 결과를 표준 형식으로 변환
|
||||
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 트리거)
|
||||
try {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 제어관리 실행 (화면에 설정된 경우)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -219,7 +219,11 @@ export class EntityJoinService {
|
|||
];
|
||||
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 isJoinTableColumn = [
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function ScreenViewPage() {
|
|||
const [layout, setLayout] = useState<LayoutData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
|
||||
// 화면 너비에 따라 Y좌표 유지 여부 결정
|
||||
const [preserveYPosition, setPreserveYPosition] = useState(true);
|
||||
|
|
@ -34,7 +34,7 @@ export default function ScreenViewPage() {
|
|||
const [editModalConfig, setEditModalConfig] = useState<{
|
||||
screenId?: number;
|
||||
modalSize?: "sm" | "md" | "lg" | "xl" | "full";
|
||||
editData?: any;
|
||||
editData?: Record<string, unknown>;
|
||||
onSave?: () => void;
|
||||
modalTitle?: string;
|
||||
modalDescription?: string;
|
||||
|
|
@ -70,11 +70,11 @@ export default function ScreenViewPage() {
|
|||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - CustomEvent type
|
||||
window.addEventListener("openEditModal", handleOpenEditModal);
|
||||
|
||||
return () => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error - CustomEvent type
|
||||
window.removeEventListener("openEditModal", handleOpenEditModal);
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -96,8 +96,18 @@ export default function ScreenViewPage() {
|
|||
} catch (layoutError) {
|
||||
console.warn("레이아웃 로드 실패, 빈 레이아웃 사용:", layoutError);
|
||||
setLayout({
|
||||
screenId,
|
||||
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) {
|
||||
|
|
@ -174,6 +184,13 @@ export default function ScreenViewPage() {
|
|||
containerWidth={window.innerWidth}
|
||||
screenWidth={screenWidth}
|
||||
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 }}
|
||||
/>
|
||||
) : (
|
||||
// 빈 화면일 때
|
||||
|
|
|
|||
|
|
@ -10,12 +10,15 @@ import { Breakpoint, BREAKPOINTS } from "@/types/responsive";
|
|||
import { generateSmartDefaults, ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
|
||||
interface ResponsiveLayoutEngineProps {
|
||||
export interface ResponsiveLayoutEngineProps {
|
||||
components: ComponentData[];
|
||||
breakpoint: Breakpoint;
|
||||
containerWidth: number;
|
||||
screenWidth?: number;
|
||||
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,
|
||||
screenWidth = 1920,
|
||||
preserveYPosition = false, // 기본값: 반응형 모드 (16px 간격)
|
||||
formData,
|
||||
onFormDataChange,
|
||||
screenInfo,
|
||||
}) => {
|
||||
// 1단계: 컴포넌트들을 Y 위치 기준으로 행(row)으로 그룹화
|
||||
const rows = useMemo(() => {
|
||||
|
|
@ -72,6 +78,18 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
|
|||
const responsiveComponents = useMemo(() => {
|
||||
const result = sortedRows.flatMap((row, rowIndex) =>
|
||||
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);
|
||||
|
||||
|
|
@ -140,6 +158,7 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
|
|||
gap: "16px",
|
||||
padding: "0 16px",
|
||||
marginTop,
|
||||
alignItems: "start", // 각 아이템이 원래 높이 유지
|
||||
}}
|
||||
>
|
||||
{rowComponents.map((comp) => (
|
||||
|
|
@ -148,9 +167,19 @@ export const ResponsiveLayoutEngine: React.FC<ResponsiveLayoutEngineProps> = ({
|
|||
className="responsive-grid-item"
|
||||
style={{
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -872,7 +872,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 화면의 기본 테이블 정보 로드
|
||||
// 화면의 기본 테이블 정보 로드 (원래대로 복원)
|
||||
useEffect(() => {
|
||||
const loadScreenTable = async () => {
|
||||
const tableName = selectedScreen?.tableName;
|
||||
|
|
@ -915,7 +915,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
columns,
|
||||
};
|
||||
|
||||
setTables([tableInfo]);
|
||||
setTables([tableInfo]); // 현재 화면의 테이블만 저장 (원래대로)
|
||||
} catch (error) {
|
||||
console.error("화면 테이블 정보 로드 실패:", error);
|
||||
setTables([]);
|
||||
|
|
@ -1996,7 +1996,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
"accordion-basic": 12, // 아코디언 (100%)
|
||||
"table-list": 12, // 테이블 리스트 (100%)
|
||||
"image-display": 4, // 이미지 표시 (33%)
|
||||
"split-panel-layout": 12, // 분할 패널 레이아웃 (100%)
|
||||
"split-panel-layout": 6, // 분할 패널 레이아웃 (50%)
|
||||
|
||||
// 액션 컴포넌트 (ACTION 카테고리)
|
||||
"button-basic": 1, // 버튼 (8.33%)
|
||||
|
|
|
|||
|
|
@ -95,7 +95,14 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
|||
}
|
||||
const newItems = [...items, createEmptyItem()];
|
||||
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);
|
||||
setItems(newItems);
|
||||
onChange?.(newItems);
|
||||
|
||||
// targetTable이 설정된 경우 각 항목에 메타데이터 추가
|
||||
const dataWithMeta = config.targetTable
|
||||
? newItems.map((item) => ({ ...item, _targetTable: config.targetTable }))
|
||||
: newItems;
|
||||
|
||||
onChange?.(dataWithMeta);
|
||||
|
||||
// 접힌 상태도 업데이트
|
||||
const newCollapsed = new Set(collapsedItems);
|
||||
|
|
@ -121,7 +134,19 @@ export const RepeaterInput: React.FC<RepeaterInputProps> = ({
|
|||
[fieldName]: value,
|
||||
};
|
||||
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);
|
||||
};
|
||||
|
||||
// 접기/펼치기 토글
|
||||
|
|
|
|||
|
|
@ -18,12 +18,20 @@ export interface RepeaterConfigPanelProps {
|
|||
config: RepeaterFieldGroupConfig;
|
||||
onChange: (config: RepeaterFieldGroupConfig) => void;
|
||||
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 [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
|
||||
|
|
@ -71,8 +79,79 @@ export const RepeaterConfigPanel: React.FC<RepeaterConfigPanelProps> = ({ config
|
|||
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 (
|
||||
<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">
|
||||
<Label className="text-sm font-semibold">필드 정의</Label>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export interface DynamicComponentRendererProps {
|
|||
component: ComponentData;
|
||||
isSelected?: boolean;
|
||||
isPreview?: boolean; // 반응형 모드 플래그
|
||||
isDesignMode?: boolean; // 디자인 모드 여부 (false일 때 데이터 로드)
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
onDragStart?: (e: React.DragEvent) => void;
|
||||
onDragEnd?: () => void;
|
||||
|
|
@ -193,8 +194,34 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
autoGeneration: component.autoGeneration,
|
||||
hidden: component.hidden,
|
||||
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 구성
|
||||
const rendererProps = {
|
||||
component,
|
||||
|
|
@ -215,7 +242,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
isInteractive,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onChange: onFormDataChange, // onChange도 전달
|
||||
onChange: handleChange, // 개선된 onChange 핸들러 전달
|
||||
tableName,
|
||||
onRefresh,
|
||||
onClose,
|
||||
|
|
@ -237,6 +264,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
refreshKey,
|
||||
// 반응형 모드 플래그 전달
|
||||
isPreview,
|
||||
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
|
||||
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
|
||||
};
|
||||
|
||||
// 렌더러가 클래스인지 함수인지 확인
|
||||
|
|
|
|||
|
|
@ -88,18 +88,19 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
|
||||
// 삭제 액션 감지 로직 (실제 필드명 사용)
|
||||
const isDeleteAction = () => {
|
||||
const deleteKeywords = ['삭제', 'delete', 'remove', '제거', 'del'];
|
||||
const deleteKeywords = ["삭제", "delete", "remove", "제거", "del"];
|
||||
return (
|
||||
component.componentConfig?.action?.type === 'delete' ||
|
||||
component.config?.action?.type === 'delete' ||
|
||||
component.webTypeConfig?.actionType === 'delete' ||
|
||||
component.text?.toLowerCase().includes('삭제') ||
|
||||
component.text?.toLowerCase().includes('delete') ||
|
||||
component.label?.toLowerCase().includes('삭제') ||
|
||||
component.label?.toLowerCase().includes('delete') ||
|
||||
deleteKeywords.some(keyword =>
|
||||
component.config?.buttonText?.toLowerCase().includes(keyword) ||
|
||||
component.config?.text?.toLowerCase().includes(keyword)
|
||||
component.componentConfig?.action?.type === "delete" ||
|
||||
component.config?.action?.type === "delete" ||
|
||||
component.webTypeConfig?.actionType === "delete" ||
|
||||
component.text?.toLowerCase().includes("삭제") ||
|
||||
component.text?.toLowerCase().includes("delete") ||
|
||||
component.label?.toLowerCase().includes("삭제") ||
|
||||
component.label?.toLowerCase().includes("delete") ||
|
||||
deleteKeywords.some(
|
||||
(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 (component.style) {
|
||||
component.style.labelColor = '#ef4444';
|
||||
component.style.labelColor = "#ef4444";
|
||||
} else {
|
||||
component.style = { labelColor: '#ef4444' };
|
||||
component.style = { labelColor: "#ef4444" };
|
||||
}
|
||||
}
|
||||
}, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]);
|
||||
|
|
@ -125,20 +126,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
// 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동)
|
||||
const getLabelColor = () => {
|
||||
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 getDarkColor = (baseColor: string) => {
|
||||
const hex = baseColor.replace('#', '');
|
||||
const hex = baseColor.replace("#", "");
|
||||
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 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);
|
||||
|
|
@ -246,6 +247,23 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
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 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
|
||||
if (actionConfig.type !== "edit") {
|
||||
const successMessage =
|
||||
|
|
@ -268,10 +286,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
|
||||
// 저장/수정 성공 시 자동 처리
|
||||
if (actionConfig.type === "save" || actionConfig.type === "edit") {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
// 1. 테이블 새로고침 이벤트 먼저 발송 (모달이 닫히기 전에)
|
||||
console.log("🔄 저장/수정 후 테이블 새로고침 이벤트 발송");
|
||||
window.dispatchEvent(new CustomEvent('refreshTable'));
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
|
||||
// 2. 모달 닫기 (약간의 딜레이)
|
||||
setTimeout(() => {
|
||||
|
|
@ -280,12 +298,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
|
||||
if (isInEditModal) {
|
||||
console.log("🚪 EditModal 닫기 이벤트 발송");
|
||||
window.dispatchEvent(new CustomEvent('closeEditModal'));
|
||||
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
||||
}
|
||||
|
||||
// ScreenModal은 항상 닫기
|
||||
console.log("🚪 ScreenModal 닫기 이벤트 발송");
|
||||
window.dispatchEvent(new CustomEvent('closeSaveModal'));
|
||||
window.dispatchEvent(new CustomEvent("closeSaveModal"));
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
|
@ -301,19 +319,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
|
||||
console.error("❌ 버튼 액션 실행 오류:", error);
|
||||
|
||||
// 오류 토스트 표시
|
||||
const errorMessage =
|
||||
actionConfig.errorMessage ||
|
||||
(actionConfig.type === "save"
|
||||
? "저장 중 오류가 발생했습니다."
|
||||
: actionConfig.type === "delete"
|
||||
? "삭제 중 오류가 발생했습니다."
|
||||
: actionConfig.type === "submit"
|
||||
? "제출 중 오류가 발생했습니다."
|
||||
: "처리 중 오류가 발생했습니다.");
|
||||
|
||||
console.log("💥 오류 토스트 표시:", errorMessage);
|
||||
toast.error(errorMessage);
|
||||
// 오류 토스트는 buttonActions.ts에서 이미 표시되므로 여기서는 제거
|
||||
// (중복 토스트 방지)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -379,7 +386,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
console.log("⚠️ 액션 실행 조건 불만족:", {
|
||||
isInteractive,
|
||||
hasAction: !!processedConfig.action,
|
||||
"이유": !isInteractive ? "인터랙티브 모드 아님" : "액션 없음",
|
||||
이유: !isInteractive ? "인터랙티브 모드 아님" : "액션 없음",
|
||||
});
|
||||
// 액션이 설정되지 않은 경우 기본 onClick 실행
|
||||
onClick?.();
|
||||
|
|
@ -495,9 +502,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
margin: "0",
|
||||
lineHeight: "1",
|
||||
minHeight: "36px",
|
||||
boxShadow: componentConfig.disabled
|
||||
? "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
|
||||
: `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
|
||||
boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 2px 4px 0 ${buttonColor}33`, // 33은 20% 투명도
|
||||
// isInteractive 모드에서는 사용자 스타일 우선 적용
|
||||
...(isInteractive && component.style ? component.style : {}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class RepeaterFieldGroupRenderer extends AutoRegisteringComponentRenderer
|
|||
configPanel: RepeaterConfigPanel,
|
||||
defaultSize: {
|
||||
width: 600,
|
||||
height: 400, // 여러 항목과 필드를 표시할 수 있도록 높이 설정
|
||||
height: 200, // 기본 높이 조정
|
||||
},
|
||||
defaultConfig: {
|
||||
fields: [], // 빈 배열로 시작 - 사용자가 직접 필드 추가
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
// 컴포넌트 스타일
|
||||
const componentStyle: React.CSSProperties = isPreview
|
||||
? {
|
||||
// 반응형 모드: position relative, width/height 100%
|
||||
// 반응형 모드: position relative, 그리드 컨테이너가 제공하는 크기 사용
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
// width 제거 - 그리드 컬럼이 결정
|
||||
height: `${component.style?.height || 600}px`,
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
|
|
@ -257,19 +257,27 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={componentStyle}
|
||||
style={
|
||||
isPreview
|
||||
? {
|
||||
position: "relative",
|
||||
height: `${component.style?.height || 600}px`,
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
: componentStyle
|
||||
}
|
||||
onClick={(e) => {
|
||||
if (isDesignMode) {
|
||||
e.stopPropagation();
|
||||
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
|
||||
style={{ width: `${leftWidth}%`, minWidth: `${minLeftWidth}px` }}
|
||||
className="flex flex-col border-r border-gray-200"
|
||||
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px` }}
|
||||
className="flex flex-shrink-0 flex-col border-r border-gray-200"
|
||||
>
|
||||
<Card className="flex h-full flex-col border-0 shadow-none">
|
||||
<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">
|
||||
<CardHeader className="border-b border-gray-100 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ interface SplitPanelLayoutConfigPanelProps {
|
|||
export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
tables = [], // 기본값 빈 배열
|
||||
tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
|
||||
screenTableName, // 현재 화면의 테이블명
|
||||
}) => {
|
||||
const [rightTableOpen, setRightTableOpen] = useState(false);
|
||||
|
|
@ -36,6 +36,32 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
const [rightColumnOpen, setRightColumnOpen] = useState(false);
|
||||
const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({});
|
||||
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이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
|
|
@ -285,7 +316,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
<CommandInput placeholder="테이블 검색..." />
|
||||
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{tables.map((table) => (
|
||||
{availableRightTables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
|
|
@ -300,8 +331,8 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
|||
config.rightPanel?.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{table.tableName}
|
||||
<span className="ml-2 text-xs text-gray-500">({table.tableLabel || ""})</span>
|
||||
{table.displayName || table.tableName}
|
||||
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const SplitPanelLayoutDefinition = createComponentDefinition({
|
|||
autoLoad: true,
|
||||
syncSelection: true,
|
||||
} as SplitPanelLayoutConfig,
|
||||
defaultSize: { width: 1000, height: 600 },
|
||||
defaultSize: { width: 800, height: 600 },
|
||||
configPanel: SplitPanelLayoutConfigPanel,
|
||||
icon: "PanelLeftRight",
|
||||
tags: ["분할", "마스터", "디테일", "레이아웃"],
|
||||
|
|
|
|||
|
|
@ -155,14 +155,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onSelectedRowsChange,
|
||||
onConfigChange,
|
||||
refreshKey,
|
||||
tableName, // 화면의 기본 테이블명 (screenInfo에서 전달)
|
||||
}) => {
|
||||
// 컴포넌트 설정
|
||||
const tableConfig = {
|
||||
...config,
|
||||
...component.config,
|
||||
...componentConfig,
|
||||
// selectedTable이 없으면 화면의 기본 테이블 사용
|
||||
selectedTable:
|
||||
componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName,
|
||||
} 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 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) {
|
||||
setData([]);
|
||||
return;
|
||||
|
|
@ -448,81 +448,54 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
try {
|
||||
// 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회
|
||||
console.log("🔗 Entity 조인 데이터 조회 시작:", tableConfig.selectedTable);
|
||||
|
||||
// Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들)
|
||||
const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || [];
|
||||
|
||||
// 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만)
|
||||
const joinTabColumns =
|
||||
tableConfig.columns?.filter(
|
||||
(col) =>
|
||||
!col.isEntityJoin &&
|
||||
col.columnName.includes("_") &&
|
||||
(col.columnName.includes("dept_code_") ||
|
||||
col.columnName.includes("_dept_code") ||
|
||||
col.columnName.includes("_company_") ||
|
||||
col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들
|
||||
) || [];
|
||||
// 🎯 조인 탭에서 추가한 컬럼들 추출 (additionalJoinInfo가 있는 컬럼들)
|
||||
const manualJoinColumns =
|
||||
tableConfig.columns?.filter((col) => {
|
||||
return col.additionalJoinInfo !== undefined;
|
||||
}) || [];
|
||||
|
||||
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,
|
||||
sourceColumn: col.entityJoinInfo!.sourceColumn,
|
||||
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";
|
||||
sourceColumn = "dept_code";
|
||||
}
|
||||
|
||||
console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`);
|
||||
|
||||
return {
|
||||
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 || " - ",
|
||||
};
|
||||
// 수동 조인 컬럼들 - 저장된 조인 정보 사용
|
||||
manualJoinColumns.forEach((col) => {
|
||||
if (col.additionalJoinInfo) {
|
||||
additionalJoinColumns.push({
|
||||
sourceTable: col.additionalJoinInfo.sourceTable,
|
||||
sourceColumn: col.additionalJoinInfo.sourceColumn,
|
||||
joinAlias: col.additionalJoinInfo.joinAlias,
|
||||
referenceTable: col.additionalJoinInfo.referenceTable,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log("🔗 Entity 조인 컬럼:", entityJoinColumns);
|
||||
console.log("🔗 조인 탭 컬럼:", joinTabColumns);
|
||||
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
|
||||
// console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs);
|
||||
console.log("🔗 최종 추가 조인 컬럼:", additionalJoinColumns);
|
||||
|
||||
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
|
||||
page: currentPage,
|
||||
|
|
@ -591,7 +564,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
sortOrder: sortDirection,
|
||||
enableEntityJoin: true, // 🎯 Entity 조인 활성화
|
||||
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
|
||||
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정
|
||||
});
|
||||
|
||||
if (result) {
|
||||
|
|
@ -661,16 +633,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const actualApiColumns = Object.keys(result.data[0]);
|
||||
console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns);
|
||||
|
||||
// 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답)
|
||||
// 실제 API 응답에 존재하는 컬럼만 매핑
|
||||
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가 조인되지 않음)
|
||||
dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음)
|
||||
dept_code_name: "dept_code_name", // dept_info.dept_name
|
||||
dept_name: "dept_name", // dept_info.dept_name
|
||||
status: "status", // user_info.status
|
||||
};
|
||||
// 🎯 조인 컬럼 매핑 테이블 - 동적 생성
|
||||
// API 응답에 실제로 존재하는 컬럼과 사용자 설정 컬럼을 비교하여 자동 매핑
|
||||
const newJoinColumnMapping: Record<string, string> = {};
|
||||
|
||||
processedColumns.forEach((col) => {
|
||||
// API 응답에 정확히 일치하는 컬럼이 있으면 그대로 사용
|
||||
if (actualApiColumns.includes(col.columnName)) {
|
||||
newJoinColumnMapping[col.columnName] = col.columnName;
|
||||
}
|
||||
});
|
||||
|
||||
// 🎯 조인 컬럼 매핑 상태 업데이트
|
||||
setJoinColumnMapping(newJoinColumnMapping);
|
||||
|
|
@ -795,7 +767,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
} finally {
|
||||
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) => {
|
||||
|
|
@ -947,12 +949,37 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
}
|
||||
}, [columnLabels]);
|
||||
|
||||
// 🎯 컬럼 개수와 컬럼명을 문자열로 변환하여 의존성 추적
|
||||
const columnsKey = useMemo(() => {
|
||||
if (!tableConfig.columns) return "";
|
||||
return tableConfig.columns.map((col) => col.columnName).join(",");
|
||||
}, [tableConfig.columns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableConfig.autoLoad && !isDesignMode) {
|
||||
fetchTableDataDebounced();
|
||||
// autoLoad가 undefined거나 true일 때 자동 로드 (기본값: true)
|
||||
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,
|
||||
columnsKey, // 🎯 컬럼이 추가/변경될 때 데이터 다시 로드 (문자열 비교)
|
||||
localPageSize,
|
||||
currentPage,
|
||||
searchTerm,
|
||||
|
|
@ -960,6 +987,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
sortDirection,
|
||||
columnLabels,
|
||||
searchValues,
|
||||
fetchTableDataInternal, // 의존성 배열에 추가
|
||||
]);
|
||||
|
||||
// refreshKey 변경 시 테이블 데이터 새로고침
|
||||
|
|
@ -1314,35 +1342,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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 (
|
||||
<div
|
||||
style={{ ...componentStyle, zIndex: 10 }} // 🎯 componentStyle + z-index 추가
|
||||
className={cn(
|
||||
"relative overflow-hidden",
|
||||
"bg-white border border-gray-200/60",
|
||||
"border border-gray-200/60 bg-white",
|
||||
"rounded-2xl shadow-sm",
|
||||
"backdrop-blur-sm",
|
||||
"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,
|
||||
)}
|
||||
{...domProps}
|
||||
|
|
@ -1359,7 +1370,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
{(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>
|
||||
|
||||
|
|
@ -1377,16 +1388,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
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="relative">
|
||||
<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>}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{loading ? "새로고침 중..." : "새로고침"}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-700">{loading ? "새로고침 중..." : "새로고침"}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -1424,7 +1433,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
{/* 테이블 컨텐츠 */}
|
||||
<div
|
||||
className={`w-full overflow-auto flex-1`}
|
||||
className={`w-full flex-1 overflow-auto`}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
|
|
@ -1622,7 +1631,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
<TableCell
|
||||
key={column.columnName}
|
||||
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}`,
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -1687,7 +1696,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 푸터/페이지네이션 */}
|
||||
{tableConfig.showFooter && tableConfig.pagination?.enabled && (
|
||||
{/* showFooter와 pagination.enabled의 기본값은 true */}
|
||||
{tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center space-y-4 border-t border-gray-200 bg-gray-100/80 p-6"
|
||||
style={{
|
||||
|
|
@ -1749,7 +1759,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 데이터는 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) => (
|
||||
<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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(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" />
|
||||
</Button>
|
||||
|
|
@ -1775,7 +1785,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
size="sm"
|
||||
onClick={() => handlePageChange(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" />
|
||||
</Button>
|
||||
|
|
@ -1791,7 +1801,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -1800,7 +1810,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
size="sm"
|
||||
onClick={() => handlePageChange(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" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -97,13 +97,20 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
>
|
||||
>({});
|
||||
|
||||
// 화면 테이블명이 있으면 자동으로 설정
|
||||
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
|
||||
useEffect(() => {
|
||||
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
|
||||
console.log("🔄 화면 테이블명 자동 설정:", screenTableName);
|
||||
onChange({ selectedTable: screenTableName });
|
||||
if (screenTableName && !config.selectedTable) {
|
||||
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
|
||||
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(() => {
|
||||
|
|
@ -137,25 +144,32 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
screenTableName,
|
||||
);
|
||||
|
||||
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우에만 컬럼 목록 표시
|
||||
const shouldShowColumns = config.selectedTable || (screenTableName && config.columns && config.columns.length > 0);
|
||||
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우 컬럼 목록 표시
|
||||
const shouldShowColumns = config.selectedTable || screenTableName;
|
||||
|
||||
if (!shouldShowColumns) {
|
||||
console.log("🔧 컬럼 목록 숨김 - 명시적 테이블 선택 또는 설정된 컬럼이 없음");
|
||||
console.log("🔧 컬럼 목록 숨김 - 테이블이 선택되지 않음");
|
||||
setAvailableColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// tableColumns prop을 우선 사용하되, 컴포넌트가 명시적으로 설정되었을 때만
|
||||
if (tableColumns && tableColumns.length > 0 && (config.selectedTable || config.columns?.length > 0)) {
|
||||
console.log("🔧 tableColumns prop 사용:", tableColumns);
|
||||
// tableColumns prop을 우선 사용
|
||||
if (tableColumns && tableColumns.length > 0) {
|
||||
const mappedColumns = tableColumns.map((column: any) => ({
|
||||
columnName: column.columnName || column.name,
|
||||
dataType: column.dataType || column.type || "text",
|
||||
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
|
||||
}));
|
||||
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
|
||||
setAvailableColumns(mappedColumns);
|
||||
|
||||
// selectedTable이 없으면 screenTableName으로 설정
|
||||
if (!config.selectedTable && screenTableName) {
|
||||
onChange({
|
||||
...config,
|
||||
selectedTable: screenTableName,
|
||||
columns: config.columns || [],
|
||||
});
|
||||
}
|
||||
} else if (config.selectedTable || screenTableName) {
|
||||
// API에서 컬럼 정보 가져오기
|
||||
const fetchColumns = async () => {
|
||||
|
|
@ -190,7 +204,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
} else {
|
||||
setAvailableColumns([]);
|
||||
}
|
||||
}, [config.selectedTable, screenTableName, tableColumns, config.columns]);
|
||||
}, [config.selectedTable, screenTableName, tableColumns]);
|
||||
|
||||
// Entity 조인 컬럼 정보 가져오기
|
||||
useEffect(() => {
|
||||
|
|
@ -275,8 +289,30 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
|
||||
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
|
||||
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);
|
||||
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)
|
||||
const newColumn: ColumnConfig = {
|
||||
|
|
@ -289,10 +325,21 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
format: "text",
|
||||
order: config.columns?.length || 0,
|
||||
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]);
|
||||
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 플래그 설정
|
||||
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
|
||||
const prevColumnsLengthRef = React.useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const currentLength = config.columns?.length || 0;
|
||||
const prevLength = prevColumnsLengthRef.current;
|
||||
|
||||
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
|
||||
hasColumns: !!config.columns,
|
||||
columnsCount: config.columns?.length || 0,
|
||||
columnsCount: currentLength,
|
||||
prevColumnsCount: prevLength,
|
||||
hasTableColumns: !!tableColumns,
|
||||
tableColumnsCount: tableColumns?.length || 0,
|
||||
selectedTable: config.selectedTable,
|
||||
});
|
||||
|
||||
if (!config.columns || !tableColumns) {
|
||||
if (!config.columns || !tableColumns || config.columns.length === 0) {
|
||||
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
|
||||
prevColumnsLengthRef.current = currentLength;
|
||||
return;
|
||||
}
|
||||
|
||||
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
|
||||
if (currentLength === prevLength && prevLength > 0) {
|
||||
console.log("ℹ️ 컬럼 개수 변경 없음, 엔티티 감지 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -352,14 +413,14 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
...column,
|
||||
isEntityJoin: true,
|
||||
entityJoinInfo: {
|
||||
sourceTable: config.selectedTable || "",
|
||||
sourceTable: config.selectedTable || screenTableName || "",
|
||||
sourceColumn: column.columnName,
|
||||
joinAlias: column.columnName,
|
||||
},
|
||||
entityDisplayConfig: {
|
||||
displayColumns: [], // 빈 배열로 초기화
|
||||
separator: " - ",
|
||||
sourceTable: config.selectedTable || "",
|
||||
sourceTable: config.selectedTable || screenTableName || "",
|
||||
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
|
||||
},
|
||||
};
|
||||
|
|
@ -377,7 +438,11 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
} else {
|
||||
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) => {
|
||||
|
|
@ -400,6 +465,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
// entityDisplayConfig가 없으면 초기화
|
||||
if (!column.entityDisplayConfig) {
|
||||
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) => {
|
||||
if (col.columnName === column.columnName) {
|
||||
return {
|
||||
|
|
@ -407,7 +481,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
entityDisplayConfig: {
|
||||
displayColumns: [],
|
||||
separator: " - ",
|
||||
sourceTable: config.selectedTable || "",
|
||||
sourceTable: initialSourceTable,
|
||||
joinTable: "",
|
||||
},
|
||||
};
|
||||
|
|
@ -430,15 +504,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
|
||||
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
|
||||
|
||||
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기
|
||||
let sourceTable = column.entityDisplayConfig.sourceTable;
|
||||
// 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;
|
||||
|
||||
if (!sourceTable && column.entityJoinInfo) {
|
||||
sourceTable = column.entityJoinInfo.sourceTable;
|
||||
// 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로 조회해서 설정
|
||||
try {
|
||||
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
|
||||
|
|
@ -464,10 +557,15 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
if (updatedColumns) {
|
||||
handleChange("columns", updatedColumns);
|
||||
}
|
||||
} else {
|
||||
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||||
console.log("❌ 조회 실패 상세:", { sourceTable, columnName: column.columnName });
|
||||
}
|
||||
} else if (!joinTable) {
|
||||
console.warn("⚠️ sourceTable이 없어서 joinTable 조회 불가:", column.columnName);
|
||||
}
|
||||
|
||||
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
|
||||
|
|
@ -795,9 +893,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Label htmlFor="cards-per-row">한 행당 카드 수</Label>
|
||||
<Select
|
||||
value={config.cardConfig?.cardsPerRow?.toString() || "3"}
|
||||
onValueChange={(value) =>
|
||||
handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))
|
||||
}
|
||||
onValueChange={(value) => handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
|
|
@ -819,9 +915,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
id="card-spacing"
|
||||
type="number"
|
||||
value={config.cardConfig?.cardSpacing || 16}
|
||||
onChange={(e) =>
|
||||
handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))
|
||||
}
|
||||
onChange={(e) => handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))}
|
||||
min="0"
|
||||
max="50"
|
||||
/>
|
||||
|
|
@ -836,9 +930,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Label htmlFor="id-column">ID 컬럼 (사번 등)</Label>
|
||||
<Select
|
||||
value={config.cardConfig?.idColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
handleNestedChange("cardConfig", "idColumn", value)
|
||||
}
|
||||
onValueChange={(value) => handleNestedChange("cardConfig", "idColumn", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ID 컬럼 선택" />
|
||||
|
|
@ -857,9 +949,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Label htmlFor="title-column">제목 컬럼 (이름 등)</Label>
|
||||
<Select
|
||||
value={config.cardConfig?.titleColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
handleNestedChange("cardConfig", "titleColumn", value)
|
||||
}
|
||||
onValueChange={(value) => handleNestedChange("cardConfig", "titleColumn", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="제목 컬럼 선택" />
|
||||
|
|
@ -878,9 +968,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Label htmlFor="subtitle-column">서브 제목 컬럼 (부서 등)</Label>
|
||||
<Select
|
||||
value={config.cardConfig?.subtitleColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
handleNestedChange("cardConfig", "subtitleColumn", value)
|
||||
}
|
||||
onValueChange={(value) => handleNestedChange("cardConfig", "subtitleColumn", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
|
||||
|
|
@ -900,9 +988,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Label htmlFor="description-column">설명 컬럼</Label>
|
||||
<Select
|
||||
value={config.cardConfig?.descriptionColumn || ""}
|
||||
onValueChange={(value) =>
|
||||
handleNestedChange("cardConfig", "descriptionColumn", value)
|
||||
}
|
||||
onValueChange={(value) => handleNestedChange("cardConfig", "descriptionColumn", value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
|
||||
|
|
@ -1270,7 +1356,34 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
|
|||
<Button
|
||||
variant="outline"
|
||||
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"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -72,21 +72,29 @@ export interface ColumnConfig {
|
|||
// 새로운 기능들
|
||||
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
|
||||
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
|
||||
|
||||
// 🎯 추가 조인 컬럼 정보 (조인 탭에서 추가한 컬럼들)
|
||||
additionalJoinInfo?: {
|
||||
sourceTable: string; // 원본 테이블
|
||||
sourceColumn: string; // 원본 컬럼 (예: dept_code)
|
||||
referenceTable?: string; // 참조 테이블 (예: dept_info)
|
||||
joinAlias: string; // 조인 별칭 (예: dept_code_company_name)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 디스플레이 설정
|
||||
*/
|
||||
export interface CardDisplayConfig {
|
||||
idColumn: string; // ID 컬럼 (사번 등)
|
||||
titleColumn: string; // 제목 컬럼 (이름 등)
|
||||
subtitleColumn?: string; // 부제목 컬럼 (부서 등)
|
||||
idColumn: string; // ID 컬럼 (사번 등)
|
||||
titleColumn: string; // 제목 컬럼 (이름 등)
|
||||
subtitleColumn?: string; // 부제목 컬럼 (부서 등)
|
||||
descriptionColumn?: string; // 설명 컬럼
|
||||
imageColumn?: string; // 이미지 컬럼
|
||||
cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
|
||||
cardSpacing: number; // 카드 간격 (기본: 16px)
|
||||
showActions: boolean; // 액션 버튼 표시 여부
|
||||
cardHeight?: number; // 카드 높이 (기본: auto)
|
||||
imageColumn?: string; // 이미지 컬럼
|
||||
cardsPerRow: number; // 한 행당 카드 수 (기본: 3)
|
||||
cardSpacing: number; // 카드 간격 (기본: 16px)
|
||||
showActions: boolean; // 액션 버튼 표시 여부
|
||||
cardHeight?: number; // 카드 높이 (기본: auto)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -163,7 +171,7 @@ export interface CheckboxConfig {
|
|||
*/
|
||||
export interface TableListConfig extends ComponentConfig {
|
||||
// 표시 모드 설정
|
||||
displayMode?: "table" | "card"; // 기본: "table"
|
||||
displayMode?: "table" | "card"; // 기본: "table"
|
||||
|
||||
// 카드 디스플레이 설정 (displayMode가 "card"일 때 사용)
|
||||
cardConfig?: CardDisplayConfig;
|
||||
|
|
|
|||
|
|
@ -210,6 +210,12 @@ export class EnhancedFormService {
|
|||
* 테이블 컬럼 정보 조회 (캐시 포함)
|
||||
*/
|
||||
private async getTableColumns(tableName: string): Promise<ColumnInfo[]> {
|
||||
// tableName이 비어있으면 빈 배열 반환
|
||||
if (!tableName || tableName.trim() === "") {
|
||||
console.warn("⚠️ getTableColumns: tableName이 비어있음");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 캐시 확인
|
||||
const cached = this.columnCache.get(tableName);
|
||||
if (cached) {
|
||||
|
|
|
|||
|
|
@ -119,9 +119,12 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
}) => {
|
||||
console.log(`🔥 DynamicComponentConfigPanel 렌더링 시작: ${componentId}`);
|
||||
|
||||
// 모든 useState를 최상단에 선언 (Hooks 규칙)
|
||||
const [ConfigPanelComponent, setConfigPanelComponent] = React.useState<React.ComponentType<any> | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [selectedTableColumns, setSelectedTableColumns] = React.useState(tableColumns);
|
||||
const [allTablesList, setAllTablesList] = React.useState<any[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let mounted = true;
|
||||
|
|
@ -155,6 +158,29 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
};
|
||||
}, [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) {
|
||||
return (
|
||||
<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,
|
||||
tables: Array.isArray(tables) ? tables.length : 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 (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onChange={onChange}
|
||||
onConfigChange={onChange} // TableListConfigPanel을 위한 추가 prop
|
||||
screenTableName={screenTableName}
|
||||
tableColumns={tableColumns}
|
||||
tables={tables} // 전체 테이블 목록 전달
|
||||
tableColumns={selectedTableColumns} // 동적으로 변경되는 컬럼 전달
|
||||
tables={tables} // 기본 테이블 목록 (현재 화면의 테이블만)
|
||||
allTables={componentId === "repeater-field-group" ? allTablesList : tables} // RepeaterConfigPanel만 전체 테이블
|
||||
onTableChange={handleTableChange} // 테이블 변경 핸들러 전달
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ export function generateSmartDefaults(
|
|||
screenWidth: number = 1920,
|
||||
rowComponentCount: number = 1, // 같은 행에 있는 컴포넌트 개수
|
||||
): ResponsiveComponentConfig["responsive"] {
|
||||
// 특정 컴포넌트는 항상 전체 너비 (split-panel-layout, datatable 등)
|
||||
const fullWidthComponents = ["split-panel-layout", "datatable", "data-table"];
|
||||
// 특정 컴포넌트는 항상 전체 너비 (datatable, table-list 등)
|
||||
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;
|
||||
|
||||
if (fullWidthComponents.includes(componentType)) {
|
||||
if (fullWidthComponents.includes(componentId) || fullWidthComponents.includes(componentType)) {
|
||||
return {
|
||||
desktop: {
|
||||
gridColumns: 12, // 전체 너비
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface RepeaterFieldDefinition {
|
|||
*/
|
||||
export interface RepeaterFieldGroupConfig {
|
||||
fields: RepeaterFieldDefinition[]; // 반복될 필드 정의
|
||||
targetTable?: string; // 저장할 대상 테이블 (미지정 시 메인 화면 테이블)
|
||||
minItems?: number; // 최소 항목 수
|
||||
maxItems?: number; // 최대 항목 수
|
||||
addButtonText?: string; // 추가 버튼 텍스트
|
||||
|
|
|
|||
Loading…
Reference in New Issue