mhkim-node #412
|
|
@ -375,12 +375,15 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
// Entity 조인 컬럼 토글 (추가/제거)
|
||||
const toggleEntityJoinColumn = useCallback(
|
||||
(joinTableName: string, sourceColumn: string, refColumnName: string, refColumnLabel: string, displayField: string) => {
|
||||
(joinTableName: string, sourceColumn: string, refColumnName: string, refColumnLabel: string, displayField: string, columnType?: string) => {
|
||||
const currentJoins = config.entityJoins || [];
|
||||
const existingJoinIdx = currentJoins.findIndex(
|
||||
(j) => j.sourceColumn === sourceColumn && j.referenceTable === joinTableName,
|
||||
);
|
||||
|
||||
let newEntityJoins = [...currentJoins];
|
||||
let newColumns = [...config.columns];
|
||||
|
||||
if (existingJoinIdx >= 0) {
|
||||
const existingJoin = currentJoins[existingJoinIdx];
|
||||
const existingColIdx = existingJoin.columns.findIndex((c) => c.referenceField === refColumnName);
|
||||
|
|
@ -388,34 +391,49 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
if (existingColIdx >= 0) {
|
||||
const updatedColumns = existingJoin.columns.filter((_, i) => i !== existingColIdx);
|
||||
if (updatedColumns.length === 0) {
|
||||
updateConfig({ entityJoins: currentJoins.filter((_, i) => i !== existingJoinIdx) });
|
||||
newEntityJoins = newEntityJoins.filter((_, i) => i !== existingJoinIdx);
|
||||
} else {
|
||||
const updated = [...currentJoins];
|
||||
updated[existingJoinIdx] = { ...existingJoin, columns: updatedColumns };
|
||||
updateConfig({ entityJoins: updated });
|
||||
newEntityJoins[existingJoinIdx] = { ...existingJoin, columns: updatedColumns };
|
||||
}
|
||||
// config.columns에서도 제거
|
||||
newColumns = newColumns.filter(c => !(c.key === displayField && c.isJoinColumn));
|
||||
} else {
|
||||
const updated = [...currentJoins];
|
||||
updated[existingJoinIdx] = {
|
||||
newEntityJoins[existingJoinIdx] = {
|
||||
...existingJoin,
|
||||
columns: [...existingJoin.columns, { referenceField: refColumnName, displayField }],
|
||||
};
|
||||
updateConfig({ entityJoins: updated });
|
||||
// config.columns에 추가
|
||||
newColumns.push({
|
||||
key: displayField,
|
||||
title: refColumnLabel,
|
||||
width: "auto",
|
||||
visible: true,
|
||||
editable: false,
|
||||
isJoinColumn: true,
|
||||
inputType: columnType || "text",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
updateConfig({
|
||||
entityJoins: [
|
||||
...currentJoins,
|
||||
{
|
||||
sourceColumn,
|
||||
referenceTable: joinTableName,
|
||||
columns: [{ referenceField: refColumnName, displayField }],
|
||||
},
|
||||
],
|
||||
newEntityJoins.push({
|
||||
sourceColumn,
|
||||
referenceTable: joinTableName,
|
||||
columns: [{ referenceField: refColumnName, displayField }],
|
||||
});
|
||||
// config.columns에 추가
|
||||
newColumns.push({
|
||||
key: displayField,
|
||||
title: refColumnLabel,
|
||||
width: "auto",
|
||||
visible: true,
|
||||
editable: false,
|
||||
isJoinColumn: true,
|
||||
inputType: columnType || "text",
|
||||
});
|
||||
}
|
||||
|
||||
updateConfig({ entityJoins: newEntityJoins, columns: newColumns });
|
||||
},
|
||||
[config.entityJoins, updateConfig],
|
||||
[config.entityJoins, config.columns, updateConfig],
|
||||
);
|
||||
|
||||
// Entity 조인에 특정 컬럼이 설정되어 있는지 확인
|
||||
|
|
@ -604,9 +622,9 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
// 컬럼 토글 (현재 테이블 컬럼 - 입력용)
|
||||
const toggleInputColumn = (column: ColumnOption) => {
|
||||
const existingIndex = config.columns.findIndex((c) => c.key === column.columnName);
|
||||
const existingIndex = config.columns.findIndex((c) => c.key === column.columnName && !c.isJoinColumn && !c.isSourceDisplay);
|
||||
if (existingIndex >= 0) {
|
||||
const newColumns = config.columns.filter((c) => c.key !== column.columnName);
|
||||
const newColumns = config.columns.filter((_, i) => i !== existingIndex);
|
||||
updateConfig({ columns: newColumns });
|
||||
} else {
|
||||
// 컬럼의 inputType과 detailSettings 정보 포함
|
||||
|
|
@ -651,7 +669,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
const isColumnAdded = (columnName: string) => {
|
||||
return config.columns.some((c) => c.key === columnName && !c.isSourceDisplay);
|
||||
return config.columns.some((c) => c.key === columnName && !c.isSourceDisplay && !c.isJoinColumn);
|
||||
};
|
||||
|
||||
const isSourceColumnSelected = (columnName: string) => {
|
||||
|
|
@ -761,10 +779,9 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="basic" className="text-xs">기본</TabsTrigger>
|
||||
<TabsTrigger value="columns" className="text-xs">컬럼</TabsTrigger>
|
||||
<TabsTrigger value="entityJoin" className="text-xs">Entity 조인</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 기본 설정 탭 */}
|
||||
|
|
@ -1365,6 +1382,84 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* ===== 🆕 Entity 조인 컬럼 (표시용) ===== */}
|
||||
<div className="space-y-2 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 text-primary" />
|
||||
<Label className="text-xs font-medium text-primary">Entity 조인 컬럼 (읽기전용)</Label>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
FK 컬럼을 기반으로 참조 테이블의 데이터를 자동으로 조회하여 표시합니다.
|
||||
</p>
|
||||
|
||||
{loadingEntityJoins ? (
|
||||
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
||||
) : entityJoinData.joinTables.length === 0 ? (
|
||||
<p className="text-muted-foreground py-2 text-xs">
|
||||
{entityJoinTargetTable
|
||||
? `${entityJoinTargetTable} 테이블에 Entity 조인 가능한 컬럼이 없습니다`
|
||||
: "저장 테이블을 먼저 설정해주세요"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entityJoinData.joinTables.map((joinTable, tableIndex) => {
|
||||
const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || "";
|
||||
|
||||
return (
|
||||
<div key={tableIndex} className="space-y-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span>{joinTable.tableName}</span>
|
||||
<span className="text-muted-foreground">({sourceColumn})</span>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const isActive = isEntityJoinColumnActive(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
);
|
||||
const matchingCol = config.columns.find((c) => c.key === column.columnName && c.isJoinColumn);
|
||||
const displayField = matchingCol?.key || column.columnName;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10/50",
|
||||
isActive && "bg-primary/10",
|
||||
)}
|
||||
onClick={() =>
|
||||
toggleEntityJoinColumn(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
column.columnLabel,
|
||||
displayField,
|
||||
column.inputType || column.dataType
|
||||
)
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isActive}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 선택된 컬럼 상세 설정 - 🆕 모든 컬럼 통합, 순서 변경 가능 */}
|
||||
{config.columns.length > 0 && (
|
||||
<>
|
||||
|
|
@ -1381,7 +1476,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md border p-2",
|
||||
col.isSourceDisplay ? "border-primary/20 bg-primary/10/50" : "border-border bg-muted/30",
|
||||
(col.isSourceDisplay || col.isJoinColumn) ? "border-primary/20 bg-primary/10/50" : "border-border bg-muted/30",
|
||||
col.hidden && "opacity-50",
|
||||
)}
|
||||
draggable
|
||||
|
|
@ -1403,7 +1498,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
|
||||
|
||||
{/* 확장/축소 버튼 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpandedColumn(expandedColumn === col.key ? null : col.key)}
|
||||
|
|
@ -1419,8 +1514,10 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
|
||||
{col.isSourceDisplay ? (
|
||||
<Link2 className="text-primary h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
||||
) : col.isJoinColumn ? (
|
||||
<Link2 className="text-amber-500 h-3 w-3 flex-shrink-0" title="Entity 조인 (읽기 전용)" />
|
||||
) : (
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<Input
|
||||
|
|
@ -1431,7 +1528,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
/>
|
||||
|
||||
{/* 히든 토글 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
|
||||
|
|
@ -1446,12 +1543,12 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
|
||||
{/* 자동입력 표시 아이콘 */}
|
||||
{!col.isSourceDisplay && col.autoFill?.type && col.autoFill.type !== "none" && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && col.autoFill?.type && col.autoFill.type !== "none" && (
|
||||
<Wand2 className="h-3 w-3 text-purple-500 flex-shrink-0" title="자동 입력" />
|
||||
)}
|
||||
|
||||
{/* 편집 가능 토글 */}
|
||||
{!col.isSourceDisplay && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateColumnProp(col.key, "editable", !(col.editable ?? true))}
|
||||
|
|
@ -1474,6 +1571,13 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
onClick={() => {
|
||||
if (col.isSourceDisplay) {
|
||||
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
||||
} else if (col.isJoinColumn) {
|
||||
const newColumns = config.columns.filter(c => c.key !== col.key);
|
||||
const newEntityJoins = config.entityJoins?.map(join => ({
|
||||
...join,
|
||||
columns: join.columns.filter(c => c.displayField !== col.key)
|
||||
})).filter(join => join.columns.length > 0);
|
||||
updateConfig({ columns: newColumns, entityJoins: newEntityJoins });
|
||||
} else {
|
||||
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
||||
}
|
||||
|
|
@ -1485,7 +1589,7 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
</div>
|
||||
|
||||
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
||||
{!col.isSourceDisplay && expandedColumn === col.key && (
|
||||
{(!col.isSourceDisplay && !col.isJoinColumn) && expandedColumn === col.key && (
|
||||
<div className="ml-6 space-y-2 rounded-md border border-dashed border-input bg-muted p-2">
|
||||
{/* 자동 입력 설정 */}
|
||||
<div className="space-y-1">
|
||||
|
|
@ -1812,120 +1916,6 @@ export const V2RepeaterConfigPanel: React.FC<V2RepeaterConfigPanelProps> = ({
|
|||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Entity 조인 설정 탭 */}
|
||||
<TabsContent value="entityJoin" className="mt-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Entity 조인 연결</h3>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
FK 컬럼을 기반으로 참조 테이블의 데이터를 자동으로 조회하여 표시합니다
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-border" />
|
||||
|
||||
{loadingEntityJoins ? (
|
||||
<p className="text-muted-foreground py-2 text-center text-xs">로딩 중...</p>
|
||||
) : entityJoinData.joinTables.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-4 text-center">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{entityJoinTargetTable
|
||||
? `${entityJoinTargetTable} 테이블에 Entity 조인 가능한 컬럼이 없습니다`
|
||||
: "저장 테이블을 먼저 설정해주세요"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{entityJoinData.joinTables.map((joinTable, tableIndex) => {
|
||||
const sourceColumn = (joinTable as any).joinConfig?.sourceColumn || "";
|
||||
|
||||
return (
|
||||
<div key={tableIndex} className="space-y-1">
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-primary">
|
||||
<Link2 className="h-3 w-3" />
|
||||
<span>{joinTable.tableName}</span>
|
||||
<span className="text-muted-foreground">({sourceColumn})</span>
|
||||
</div>
|
||||
<div className="max-h-40 space-y-0.5 overflow-y-auto rounded-md border border-primary/20 bg-primary/10/30 p-2">
|
||||
{joinTable.availableColumns.map((column, colIndex) => {
|
||||
const isActive = isEntityJoinColumnActive(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
);
|
||||
const matchingCol = config.columns.find((c) => c.key === column.columnName);
|
||||
const displayField = matchingCol?.key || column.columnName;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-primary/10/50",
|
||||
isActive && "bg-primary/10",
|
||||
)}
|
||||
onClick={() =>
|
||||
toggleEntityJoinColumn(
|
||||
joinTable.tableName,
|
||||
sourceColumn,
|
||||
column.columnName,
|
||||
column.columnLabel,
|
||||
displayField,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isActive}
|
||||
className="pointer-events-none h-3.5 w-3.5"
|
||||
/>
|
||||
<Link2 className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||
<span className="truncate text-xs">{column.columnLabel}</span>
|
||||
<span className="ml-auto text-[10px] text-primary/80">
|
||||
{column.inputType || column.dataType}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 현재 설정된 Entity 조인 목록 */}
|
||||
{config.entityJoins && config.entityJoins.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium">설정된 조인</h4>
|
||||
<div className="space-y-1">
|
||||
{config.entityJoins.map((join, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1 rounded border bg-muted/30 px-2 py-1 text-[10px]">
|
||||
<Database className="h-3 w-3 text-primary" />
|
||||
<span className="font-medium">{join.sourceColumn}</span>
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<span>{join.referenceTable}</span>
|
||||
<span className="text-muted-foreground">
|
||||
({join.columns.map((c) => c.referenceField).join(", ")})
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
updateConfig({
|
||||
entityJoins: config.entityJoins!.filter((_, i) => i !== idx),
|
||||
});
|
||||
}}
|
||||
className="ml-auto h-4 w-4 p-0 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue