parent
f7e3c1924c
commit
963e0c2d24
|
|
@ -424,18 +424,16 @@ export class EntityJoinController {
|
||||||
config.referenceTable
|
config.referenceTable
|
||||||
);
|
);
|
||||||
|
|
||||||
// 현재 display_column으로 사용 중인 컬럼 제외
|
// 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
|
||||||
const currentDisplayColumn =
|
const currentDisplayColumn =
|
||||||
config.displayColumn || config.displayColumns[0];
|
config.displayColumn || config.displayColumns[0];
|
||||||
const availableColumns = columns.filter(
|
|
||||||
(col) => col.columnName !== currentDisplayColumn
|
// 모든 컬럼 표시 (기본 표시 컬럼도 포함)
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
joinConfig: config,
|
joinConfig: config,
|
||||||
tableName: config.referenceTable,
|
tableName: config.referenceTable,
|
||||||
currentDisplayColumn: currentDisplayColumn,
|
currentDisplayColumn: currentDisplayColumn,
|
||||||
availableColumns: availableColumns.map((col) => ({
|
availableColumns: columns.map((col) => ({
|
||||||
columnName: col.columnName,
|
columnName: col.columnName,
|
||||||
columnLabel: col.displayName || col.columnName,
|
columnLabel: col.displayName || col.columnName,
|
||||||
dataType: col.dataType,
|
dataType: col.dataType,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo, useCallback, useRef } from "react"
|
||||||
import { ComponentRendererProps } from "@/types/component";
|
import { ComponentRendererProps } from "@/types/component";
|
||||||
import { CardDisplayConfig } from "./types";
|
import { CardDisplayConfig } from "./types";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
import { getFullImageUrl, apiClient } from "@/lib/api/client";
|
import { getFullImageUrl, apiClient } from "@/lib/api/client";
|
||||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
|
@ -308,10 +309,35 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
|
||||||
search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined,
|
search: Object.keys(linkedFilterValues).length > 0 ? linkedFilterValues : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 조인 컬럼 설정 가져오기 (componentConfig에서)
|
||||||
|
const joinColumnsConfig = component.componentConfig?.joinColumns || [];
|
||||||
|
const entityJoinColumns = joinColumnsConfig
|
||||||
|
.filter((col: any) => col.isJoinColumn)
|
||||||
|
.map((col: any) => ({
|
||||||
|
columnName: col.columnName,
|
||||||
|
sourceColumn: col.sourceColumn,
|
||||||
|
referenceTable: col.referenceTable,
|
||||||
|
referenceColumn: col.referenceColumn,
|
||||||
|
displayColumn: col.referenceColumn,
|
||||||
|
label: col.label,
|
||||||
|
joinAlias: col.columnName, // 백엔드에서 필요한 joinAlias 추가
|
||||||
|
sourceTable: tableNameToUse, // 기준 테이블
|
||||||
|
}));
|
||||||
|
|
||||||
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
|
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
|
||||||
const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([
|
// 조인 컬럼이 있으면 entityJoinApi 사용
|
||||||
tableTypeApi.getTableData(tableNameToUse, apiParams),
|
let dataResponse;
|
||||||
|
if (entityJoinColumns.length > 0) {
|
||||||
|
console.log("🔗 [CardDisplay] 엔티티 조인 API 사용:", entityJoinColumns);
|
||||||
|
dataResponse = await entityJoinApi.getTableDataWithJoins(tableNameToUse, {
|
||||||
|
...apiParams,
|
||||||
|
additionalJoinColumns: entityJoinColumns,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dataResponse = await tableTypeApi.getTableData(tableNameToUse, apiParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [columnsResponse, inputTypesResponse] = await Promise.all([
|
||||||
tableTypeApi.getColumns(tableNameToUse),
|
tableTypeApi.getColumns(tableNameToUse),
|
||||||
tableTypeApi.getColumnInputTypes(tableNameToUse),
|
tableTypeApi.getColumnInputTypes(tableNameToUse),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,21 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
|
||||||
interface CardDisplayConfigPanelProps {
|
interface CardDisplayConfigPanelProps {
|
||||||
config: any;
|
config: any;
|
||||||
|
|
@ -9,9 +24,32 @@ interface CardDisplayConfigPanelProps {
|
||||||
tableColumns?: any[];
|
tableColumns?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EntityJoinColumn {
|
||||||
|
tableName: string;
|
||||||
|
columnName: string;
|
||||||
|
columnLabel: string;
|
||||||
|
dataType: string;
|
||||||
|
joinAlias: string;
|
||||||
|
suggestedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JoinTable {
|
||||||
|
tableName: string;
|
||||||
|
currentDisplayColumn: string;
|
||||||
|
joinConfig?: {
|
||||||
|
sourceColumn: string;
|
||||||
|
};
|
||||||
|
availableColumns: Array<{
|
||||||
|
columnName: string;
|
||||||
|
columnLabel: string;
|
||||||
|
dataType: string;
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CardDisplay 설정 패널
|
* CardDisplay 설정 패널
|
||||||
* 카드 레이아웃과 동일한 설정 UI 제공
|
* 카드 레이아웃과 동일한 설정 UI 제공 + 엔티티 조인 컬럼 지원
|
||||||
*/
|
*/
|
||||||
export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
config,
|
config,
|
||||||
|
|
@ -19,6 +57,40 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
screenTableName,
|
screenTableName,
|
||||||
tableColumns = [],
|
tableColumns = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
// 엔티티 조인 컬럼 상태
|
||||||
|
const [entityJoinColumns, setEntityJoinColumns] = useState<{
|
||||||
|
availableColumns: EntityJoinColumn[];
|
||||||
|
joinTables: JoinTable[];
|
||||||
|
}>({ availableColumns: [], joinTables: [] });
|
||||||
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||||
|
|
||||||
|
// 엔티티 조인 컬럼 정보 가져오기
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEntityJoinColumns = async () => {
|
||||||
|
const tableName = config.tableName || screenTableName;
|
||||||
|
if (!tableName) {
|
||||||
|
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingEntityJoins(true);
|
||||||
|
try {
|
||||||
|
const result = await entityJoinApi.getEntityJoinColumns(tableName);
|
||||||
|
setEntityJoinColumns({
|
||||||
|
availableColumns: result.availableColumns || [],
|
||||||
|
joinTables: result.joinTables || [],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Entity 조인 컬럼 조회 오류:", error);
|
||||||
|
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
||||||
|
} finally {
|
||||||
|
setLoadingEntityJoins(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEntityJoinColumns();
|
||||||
|
}, [config.tableName, screenTableName]);
|
||||||
|
|
||||||
const handleChange = (key: string, value: any) => {
|
const handleChange = (key: string, value: any) => {
|
||||||
onChange({ ...config, [key]: value });
|
onChange({ ...config, [key]: value });
|
||||||
};
|
};
|
||||||
|
|
@ -28,7 +100,6 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
let newConfig = { ...config };
|
let newConfig = { ...config };
|
||||||
let current = newConfig;
|
let current = newConfig;
|
||||||
|
|
||||||
// 중첩 객체 생성
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
if (!current[keys[i]]) {
|
if (!current[keys[i]]) {
|
||||||
current[keys[i]] = {};
|
current[keys[i]] = {};
|
||||||
|
|
@ -40,6 +111,47 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
onChange(newConfig);
|
onChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 컬럼 선택 시 조인 컬럼이면 joinColumns 설정도 함께 업데이트
|
||||||
|
const handleColumnSelect = (path: string, columnName: string) => {
|
||||||
|
const joinColumn = entityJoinColumns.availableColumns.find(
|
||||||
|
(col) => col.joinAlias === columnName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (joinColumn) {
|
||||||
|
const joinColumnsConfig = config.joinColumns || [];
|
||||||
|
const existingJoinColumn = joinColumnsConfig.find(
|
||||||
|
(jc: any) => jc.columnName === columnName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingJoinColumn) {
|
||||||
|
const joinTableInfo = entityJoinColumns.joinTables?.find(
|
||||||
|
(jt) => jt.tableName === joinColumn.tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
const newJoinColumnConfig = {
|
||||||
|
columnName: joinColumn.joinAlias,
|
||||||
|
label: joinColumn.suggestedLabel || joinColumn.columnLabel,
|
||||||
|
sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "",
|
||||||
|
referenceTable: joinColumn.tableName,
|
||||||
|
referenceColumn: joinColumn.columnName,
|
||||||
|
isJoinColumn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
columnMapping: {
|
||||||
|
...config.columnMapping,
|
||||||
|
[path.split(".")[1]]: columnName,
|
||||||
|
},
|
||||||
|
joinColumns: [...joinColumnsConfig, newJoinColumnConfig],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNestedChange(path, columnName);
|
||||||
|
};
|
||||||
|
|
||||||
// 표시 컬럼 추가
|
// 표시 컬럼 추가
|
||||||
const addDisplayColumn = () => {
|
const addDisplayColumn = () => {
|
||||||
const currentColumns = config.columnMapping?.displayColumns || [];
|
const currentColumns = config.columnMapping?.displayColumns || [];
|
||||||
|
|
@ -58,122 +170,198 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
const updateDisplayColumn = (index: number, value: string) => {
|
const updateDisplayColumn = (index: number, value: string) => {
|
||||||
const currentColumns = [...(config.columnMapping?.displayColumns || [])];
|
const currentColumns = [...(config.columnMapping?.displayColumns || [])];
|
||||||
currentColumns[index] = value;
|
currentColumns[index] = value;
|
||||||
|
|
||||||
|
const joinColumn = entityJoinColumns.availableColumns.find(
|
||||||
|
(col) => col.joinAlias === value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (joinColumn) {
|
||||||
|
const joinColumnsConfig = config.joinColumns || [];
|
||||||
|
const existingJoinColumn = joinColumnsConfig.find(
|
||||||
|
(jc: any) => jc.columnName === value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingJoinColumn) {
|
||||||
|
const joinTableInfo = entityJoinColumns.joinTables?.find(
|
||||||
|
(jt) => jt.tableName === joinColumn.tableName
|
||||||
|
);
|
||||||
|
|
||||||
|
const newJoinColumnConfig = {
|
||||||
|
columnName: joinColumn.joinAlias,
|
||||||
|
label: joinColumn.suggestedLabel || joinColumn.columnLabel,
|
||||||
|
sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "",
|
||||||
|
referenceTable: joinColumn.tableName,
|
||||||
|
referenceColumn: joinColumn.columnName,
|
||||||
|
isJoinColumn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
columnMapping: {
|
||||||
|
...config.columnMapping,
|
||||||
|
displayColumns: currentColumns,
|
||||||
|
},
|
||||||
|
joinColumns: [...joinColumnsConfig, newJoinColumnConfig],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleNestedChange("columnMapping.displayColumns", currentColumns);
|
handleNestedChange("columnMapping.displayColumns", currentColumns);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 테이블별로 조인 컬럼 그룹화
|
||||||
|
const joinColumnsByTable: Record<string, EntityJoinColumn[]> = {};
|
||||||
|
entityJoinColumns.availableColumns.forEach((col) => {
|
||||||
|
if (!joinColumnsByTable[col.tableName]) {
|
||||||
|
joinColumnsByTable[col.tableName] = [];
|
||||||
|
}
|
||||||
|
joinColumnsByTable[col.tableName].push(col);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컬럼 선택 셀렉트 박스 렌더링 (Shadcn UI)
|
||||||
|
const renderColumnSelect = (
|
||||||
|
value: string,
|
||||||
|
onChangeHandler: (value: string) => void,
|
||||||
|
placeholder: string = "컬럼을 선택하세요"
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={value || "__none__"}
|
||||||
|
onValueChange={(val) => onChangeHandler(val === "__none__" ? "" : val)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{/* 선택 안함 옵션 */}
|
||||||
|
<SelectItem value="__none__" className="text-xs text-muted-foreground">
|
||||||
|
선택 안함
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
{/* 기본 테이블 컬럼 */}
|
||||||
|
{tableColumns.length > 0 && (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel className="text-xs font-semibold text-muted-foreground">
|
||||||
|
기본 컬럼
|
||||||
|
</SelectLabel>
|
||||||
|
{tableColumns.map((column) => (
|
||||||
|
<SelectItem
|
||||||
|
key={column.columnName}
|
||||||
|
value={column.columnName}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{column.columnLabel || column.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 조인 테이블별 컬럼 */}
|
||||||
|
{Object.entries(joinColumnsByTable).map(([tableName, columns]) => (
|
||||||
|
<SelectGroup key={tableName}>
|
||||||
|
<SelectLabel className="text-xs font-semibold text-blue-600">
|
||||||
|
{tableName} (조인)
|
||||||
|
</SelectLabel>
|
||||||
|
{columns.map((col) => (
|
||||||
|
<SelectItem
|
||||||
|
key={col.joinAlias}
|
||||||
|
value={col.joinAlias}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{col.suggestedLabel || col.columnLabel}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-medium text-gray-700">카드 디스플레이 설정</div>
|
<div className="text-sm font-medium">카드 디스플레이 설정</div>
|
||||||
|
|
||||||
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
|
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
|
||||||
{tableColumns && tableColumns.length > 0 && (
|
{tableColumns && tableColumns.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h5 className="text-xs font-medium text-gray-700">컬럼 매핑</h5>
|
<h5 className="text-xs font-medium text-muted-foreground">컬럼 매핑</h5>
|
||||||
|
|
||||||
<div>
|
{loadingEntityJoins && (
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">타이틀 컬럼</label>
|
<div className="text-xs text-muted-foreground">조인 컬럼 로딩 중...</div>
|
||||||
<select
|
)}
|
||||||
value={config.columnMapping?.titleColumn || ""}
|
|
||||||
onChange={(e) => handleNestedChange("columnMapping.titleColumn", e.target.value)}
|
<div className="space-y-1">
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
<Label className="text-xs">타이틀 컬럼</Label>
|
||||||
>
|
{renderColumnSelect(
|
||||||
<option value="">컬럼을 선택하세요</option>
|
config.columnMapping?.titleColumn || "",
|
||||||
{tableColumns.map((column) => (
|
(value) => handleColumnSelect("columnMapping.titleColumn", value)
|
||||||
<option key={column.columnName} value={column.columnName}>
|
)}
|
||||||
{column.columnLabel || column.columnName} ({column.dataType})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">서브타이틀 컬럼</label>
|
<Label className="text-xs">서브타이틀 컬럼</Label>
|
||||||
<select
|
{renderColumnSelect(
|
||||||
value={config.columnMapping?.subtitleColumn || ""}
|
config.columnMapping?.subtitleColumn || "",
|
||||||
onChange={(e) => handleNestedChange("columnMapping.subtitleColumn", e.target.value)}
|
(value) => handleColumnSelect("columnMapping.subtitleColumn", value)
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
)}
|
||||||
>
|
|
||||||
<option value="">컬럼을 선택하세요</option>
|
|
||||||
{tableColumns.map((column) => (
|
|
||||||
<option key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnLabel || column.columnName} ({column.dataType})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">설명 컬럼</label>
|
<Label className="text-xs">설명 컬럼</Label>
|
||||||
<select
|
{renderColumnSelect(
|
||||||
value={config.columnMapping?.descriptionColumn || ""}
|
config.columnMapping?.descriptionColumn || "",
|
||||||
onChange={(e) => handleNestedChange("columnMapping.descriptionColumn", e.target.value)}
|
(value) => handleColumnSelect("columnMapping.descriptionColumn", value)
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
)}
|
||||||
>
|
|
||||||
<option value="">컬럼을 선택하세요</option>
|
|
||||||
{tableColumns.map((column) => (
|
|
||||||
<option key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnLabel || column.columnName} ({column.dataType})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">이미지 컬럼</label>
|
<Label className="text-xs">이미지 컬럼</Label>
|
||||||
<select
|
{renderColumnSelect(
|
||||||
value={config.columnMapping?.imageColumn || ""}
|
config.columnMapping?.imageColumn || "",
|
||||||
onChange={(e) => handleNestedChange("columnMapping.imageColumn", e.target.value)}
|
(value) => handleColumnSelect("columnMapping.imageColumn", value)
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
)}
|
||||||
>
|
|
||||||
<option value="">컬럼을 선택하세요</option>
|
|
||||||
{tableColumns.map((column) => (
|
|
||||||
<option key={column.columnName} value={column.columnName}>
|
|
||||||
{column.columnLabel || column.columnName} ({column.dataType})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 동적 표시 컬럼 추가 */}
|
{/* 동적 표시 컬럼 추가 */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-xs font-medium text-gray-600">표시 컬럼들</label>
|
<Label className="text-xs">표시 컬럼들</Label>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={addDisplayColumn}
|
onClick={addDisplayColumn}
|
||||||
className="rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600"
|
className="h-6 px-2 text-xs"
|
||||||
>
|
>
|
||||||
+ 컬럼 추가
|
+ 컬럼 추가
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(config.columnMapping?.displayColumns || []).map((column: string, index: number) => (
|
{(config.columnMapping?.displayColumns || []).map((column: string, index: number) => (
|
||||||
<div key={index} className="flex items-center space-x-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<select
|
<div className="flex-1">
|
||||||
value={column}
|
{renderColumnSelect(
|
||||||
onChange={(e) => updateDisplayColumn(index, e.target.value)}
|
column,
|
||||||
className="flex-1 rounded border border-gray-300 px-2 py-1 text-sm"
|
(value) => updateDisplayColumn(index, value)
|
||||||
>
|
)}
|
||||||
<option value="">컬럼을 선택하세요</option>
|
</div>
|
||||||
{tableColumns.map((col) => (
|
<Button
|
||||||
<option key={col.columnName} value={col.columnName}>
|
|
||||||
{col.columnLabel || col.columnName} ({col.dataType})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
onClick={() => removeDisplayColumn(index)}
|
onClick={() => removeDisplayColumn(index)}
|
||||||
className="rounded bg-red-500 px-2 py-1 text-xs text-white hover:bg-red-600"
|
className="h-8 w-8 p-0 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||||
>
|
>
|
||||||
삭제
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && (
|
{(!config.columnMapping?.displayColumns || config.columnMapping.displayColumns.length === 0) && (
|
||||||
<div className="rounded border border-dashed border-gray-300 py-2 text-center text-xs text-gray-500">
|
<div className="rounded-md border border-dashed border-muted-foreground/30 py-3 text-center text-xs text-muted-foreground">
|
||||||
"컬럼 추가" 버튼을 클릭하여 표시할 컬럼을 추가하세요
|
"컬럼 추가" 버튼을 클릭하여 표시할 컬럼을 추가하세요
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -184,186 +372,166 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
|
|
||||||
{/* 카드 스타일 설정 */}
|
{/* 카드 스타일 설정 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h5 className="text-xs font-medium text-gray-700">카드 스타일</h5>
|
<h5 className="text-xs font-medium text-muted-foreground">카드 스타일</h5>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">한 행당 카드 수</label>
|
<Label className="text-xs">한 행당 카드 수</Label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="6"
|
max="6"
|
||||||
value={config.cardsPerRow || 3}
|
value={config.cardsPerRow || 3}
|
||||||
onChange={(e) => handleChange("cardsPerRow", parseInt(e.target.value))}
|
onChange={(e) => handleChange("cardsPerRow", parseInt(e.target.value))}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">카드 간격 (px)</label>
|
<Label className="text-xs">카드 간격 (px)</Label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
max="50"
|
max="50"
|
||||||
value={config.cardSpacing || 16}
|
value={config.cardSpacing || 16}
|
||||||
onChange={(e) => handleChange("cardSpacing", parseInt(e.target.value))}
|
onChange={(e) => handleChange("cardSpacing", parseInt(e.target.value))}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showTitle"
|
id="showTitle"
|
||||||
checked={config.cardStyle?.showTitle ?? true}
|
checked={config.cardStyle?.showTitle ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showTitle", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showTitle", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showTitle" className="text-xs text-gray-600">
|
<Label htmlFor="showTitle" className="text-xs font-normal">
|
||||||
타이틀 표시
|
타이틀 표시
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showSubtitle"
|
id="showSubtitle"
|
||||||
checked={config.cardStyle?.showSubtitle ?? true}
|
checked={config.cardStyle?.showSubtitle ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showSubtitle", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showSubtitle", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showSubtitle" className="text-xs text-gray-600">
|
<Label htmlFor="showSubtitle" className="text-xs font-normal">
|
||||||
서브타이틀 표시
|
서브타이틀 표시
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showDescription"
|
id="showDescription"
|
||||||
checked={config.cardStyle?.showDescription ?? true}
|
checked={config.cardStyle?.showDescription ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showDescription", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showDescription", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showDescription" className="text-xs text-gray-600">
|
<Label htmlFor="showDescription" className="text-xs font-normal">
|
||||||
설명 표시
|
설명 표시
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showImage"
|
id="showImage"
|
||||||
checked={config.cardStyle?.showImage ?? false}
|
checked={config.cardStyle?.showImage ?? false}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showImage", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showImage", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showImage" className="text-xs text-gray-600">
|
<Label htmlFor="showImage" className="text-xs font-normal">
|
||||||
이미지 표시
|
이미지 표시
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showActions"
|
id="showActions"
|
||||||
checked={config.cardStyle?.showActions ?? true}
|
checked={config.cardStyle?.showActions ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showActions", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showActions", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showActions" className="text-xs text-gray-600">
|
<Label htmlFor="showActions" className="text-xs font-normal">
|
||||||
액션 버튼 표시
|
액션 버튼 표시
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 개별 버튼 설정 (액션 버튼이 활성화된 경우에만 표시) */}
|
{/* 개별 버튼 설정 */}
|
||||||
{(config.cardStyle?.showActions ?? true) && (
|
{(config.cardStyle?.showActions ?? true) && (
|
||||||
<div className="ml-4 space-y-2 border-l-2 border-gray-200 pl-3">
|
<div className="ml-5 space-y-2 border-l-2 border-muted pl-3">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showViewButton"
|
id="showViewButton"
|
||||||
checked={config.cardStyle?.showViewButton ?? true}
|
checked={config.cardStyle?.showViewButton ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showViewButton", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showViewButton", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showViewButton" className="text-xs text-gray-600">
|
<Label htmlFor="showViewButton" className="text-xs font-normal">
|
||||||
상세보기 버튼
|
상세보기 버튼
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showEditButton"
|
id="showEditButton"
|
||||||
checked={config.cardStyle?.showEditButton ?? true}
|
checked={config.cardStyle?.showEditButton ?? true}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showEditButton", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showEditButton", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showEditButton" className="text-xs text-gray-600">
|
<Label htmlFor="showEditButton" className="text-xs font-normal">
|
||||||
편집 버튼
|
편집 버튼
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="showDeleteButton"
|
id="showDeleteButton"
|
||||||
checked={config.cardStyle?.showDeleteButton ?? false}
|
checked={config.cardStyle?.showDeleteButton ?? false}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.showDeleteButton", e.target.checked)}
|
onCheckedChange={(checked) => handleNestedChange("cardStyle.showDeleteButton", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showDeleteButton" className="text-xs text-gray-600">
|
<Label htmlFor="showDeleteButton" className="text-xs font-normal">
|
||||||
삭제 버튼
|
삭제 버튼
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<label className="mb-1 block text-xs font-medium text-gray-600">설명 최대 길이</label>
|
<Label className="text-xs">설명 최대 길이</Label>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="10"
|
min="10"
|
||||||
max="500"
|
max="500"
|
||||||
value={config.cardStyle?.maxDescriptionLength || 100}
|
value={config.cardStyle?.maxDescriptionLength || 100}
|
||||||
onChange={(e) => handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))}
|
onChange={(e) => handleNestedChange("cardStyle.maxDescriptionLength", parseInt(e.target.value))}
|
||||||
className="w-full rounded border border-gray-300 px-2 py-1 text-sm"
|
className="h-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 공통 설정 */}
|
{/* 공통 설정 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h5 className="text-xs font-medium text-gray-700">공통 설정</h5>
|
<h5 className="text-xs font-medium text-muted-foreground">공통 설정</h5>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="disabled"
|
id="disabled"
|
||||||
checked={config.disabled || false}
|
checked={config.disabled || false}
|
||||||
onChange={(e) => handleChange("disabled", e.target.checked)}
|
onCheckedChange={(checked) => handleChange("disabled", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="disabled" className="text-xs text-gray-600">
|
<Label htmlFor="disabled" className="text-xs font-normal">
|
||||||
비활성화
|
비활성화
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
id="readonly"
|
id="readonly"
|
||||||
checked={config.readonly || false}
|
checked={config.readonly || false}
|
||||||
onChange={(e) => handleChange("readonly", e.target.checked)}
|
onCheckedChange={(checked) => handleChange("readonly", checked)}
|
||||||
className="rounded border-gray-300"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="readonly" className="text-xs text-gray-600">
|
<Label htmlFor="readonly" className="text-xs font-normal">
|
||||||
읽기 전용
|
읽기 전용
|
||||||
</label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue