feat(pop-string-list): 컬럼 관리 개선 및 런타임 컬럼 전환 구현
[Config 패널]
- STEP 6 리스트 레이아웃에 컬럼 추가/삭제 기능 추가
- 메인 + 조인 컬럼을 전환 후보로 확장 (기존: 조인만)
- 독립 헤더로 추가된 컬럼은 전환 후보에서 자동 제외
- STEP 3 체크 변경 시 STEP 6 순서/조인컬럼/alternateColumns 보존
- STEP 4 조인 삭제 시 listColumns/alternateColumns에서 고아 참조 자동 정리
[런타임 컴포넌트]
- 리스트 헤더에 alternateColumns 전환 UI 추가 (Popover 드롭다운)
- 조인 컬럼명 resolveColumnName 유틸 추가 ("테이블.컬럼" -> "컬럼")
- 카드 모드 텍스트 잘림 수정 (gridTemplateRows: minmax 적용)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6842a00890
commit
51e1392640
|
|
@ -9,8 +9,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { ChevronDown, ChevronUp, Loader2, AlertCircle } from "lucide-react";
|
import { ChevronDown, ChevronUp, Loader2, AlertCircle, ChevronsUpDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
import type {
|
import type {
|
||||||
PopStringListConfig,
|
PopStringListConfig,
|
||||||
|
|
@ -19,6 +25,19 @@ import type {
|
||||||
CardCellDefinition,
|
CardCellDefinition,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
// ===== 유틸리티 =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 컬럼명에서 실제 데이터 키를 추출
|
||||||
|
* 조인 컬럼은 "테이블명.컬럼명" 형식으로 저장됨 -> "컬럼명"만 추출
|
||||||
|
* 일반 컬럼은 그대로 반환
|
||||||
|
*/
|
||||||
|
function resolveColumnName(name: string): string {
|
||||||
|
if (!name) return name;
|
||||||
|
const dotIdx = name.lastIndexOf(".");
|
||||||
|
return dotIdx >= 0 ? name.substring(dotIdx + 1) : name;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Props =====
|
// ===== Props =====
|
||||||
|
|
||||||
interface PopStringListComponentProps {
|
interface PopStringListComponentProps {
|
||||||
|
|
@ -219,6 +238,10 @@ interface ListModeViewProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListModeView({ columns, data }: ListModeViewProps) {
|
function ListModeView({ columns, data }: ListModeViewProps) {
|
||||||
|
// 런타임 컬럼 전환 상태
|
||||||
|
// key: 컬럼 인덱스, value: 현재 활성 컬럼명 (alternateColumns 중 하나 또는 원래 columnName)
|
||||||
|
const [activeColumns, setActiveColumns] = useState<Record<number, string>>({});
|
||||||
|
|
||||||
if (columns.length === 0) {
|
if (columns.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center p-2">
|
<div className="flex h-full items-center justify-center p-2">
|
||||||
|
|
@ -238,7 +261,79 @@ function ListModeView({ columns, data }: ListModeViewProps) {
|
||||||
className="border-b bg-muted/50"
|
className="border-b bg-muted/50"
|
||||||
style={{ display: "grid", gridTemplateColumns: gridCols }}
|
style={{ display: "grid", gridTemplateColumns: gridCols }}
|
||||||
>
|
>
|
||||||
{columns.map((col) => (
|
{columns.map((col, colIdx) => {
|
||||||
|
const hasAlternates = (col.alternateColumns || []).length > 0;
|
||||||
|
const currentColName = activeColumns[colIdx] || col.columnName;
|
||||||
|
// 원래 컬럼이면 기존 라벨, 전환된 컬럼이면 컬럼명 부분만 표시
|
||||||
|
const currentLabel =
|
||||||
|
currentColName === col.columnName
|
||||||
|
? col.label
|
||||||
|
: resolveColumnName(currentColName);
|
||||||
|
|
||||||
|
if (hasAlternates) {
|
||||||
|
// 전환 가능한 헤더: Popover 드롭다운
|
||||||
|
return (
|
||||||
|
<Popover key={col.columnName}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center justify-between px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted/80 transition-colors"
|
||||||
|
style={{ textAlign: col.align || "left" }}
|
||||||
|
>
|
||||||
|
<span className="truncate">{currentLabel}</span>
|
||||||
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto min-w-[120px] p-1" align="start">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{/* 원래 컬럼 */}
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"rounded px-2 py-1 text-left text-xs transition-colors",
|
||||||
|
currentColName === col.columnName
|
||||||
|
? "bg-primary/10 text-primary font-medium"
|
||||||
|
: "hover:bg-muted"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveColumns((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[colIdx];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{col.label} (기본)
|
||||||
|
</button>
|
||||||
|
{/* 대체 컬럼들 */}
|
||||||
|
{(col.alternateColumns || []).map((altCol) => {
|
||||||
|
const altLabel = resolveColumnName(altCol);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={altCol}
|
||||||
|
className={cn(
|
||||||
|
"rounded px-2 py-1 text-left text-xs transition-colors",
|
||||||
|
currentColName === altCol
|
||||||
|
? "bg-primary/10 text-primary font-medium"
|
||||||
|
: "hover:bg-muted"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveColumns((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[colIdx]: altCol,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{altLabel}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전환 없는 일반 헤더
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.columnName}
|
key={col.columnName}
|
||||||
className="px-2 py-1.5 text-xs font-medium text-muted-foreground"
|
className="px-2 py-1.5 text-xs font-medium text-muted-foreground"
|
||||||
|
|
@ -246,7 +341,8 @@ function ListModeView({ columns, data }: ListModeViewProps) {
|
||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 행 */}
|
{/* 데이터 행 */}
|
||||||
|
|
@ -256,15 +352,19 @@ function ListModeView({ columns, data }: ListModeViewProps) {
|
||||||
className="border-b last:border-b-0 hover:bg-muted/30 transition-colors"
|
className="border-b last:border-b-0 hover:bg-muted/30 transition-colors"
|
||||||
style={{ display: "grid", gridTemplateColumns: gridCols }}
|
style={{ display: "grid", gridTemplateColumns: gridCols }}
|
||||||
>
|
>
|
||||||
{columns.map((col) => (
|
{columns.map((col, colIdx) => {
|
||||||
|
const currentColName = activeColumns[colIdx] || col.columnName;
|
||||||
|
const resolvedKey = resolveColumnName(currentColName);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={col.columnName}
|
key={`${col.columnName}-${colIdx}`}
|
||||||
className="px-2 py-1.5 text-xs truncate"
|
className="px-2 py-1.5 text-xs truncate"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{String(row[col.columnName] ?? "")}
|
{String(row[resolvedKey] ?? "")}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -305,14 +405,17 @@ function CardModeView({ cardGrid, data }: CardModeViewProps) {
|
||||||
cardGrid.rowHeights && cardGrid.rowHeights.length > 0
|
cardGrid.rowHeights && cardGrid.rowHeights.length > 0
|
||||||
? cardGrid.rowHeights
|
? cardGrid.rowHeights
|
||||||
.map((h) => {
|
.map((h) => {
|
||||||
if (!h) return "32px";
|
if (!h) return "minmax(32px, auto)";
|
||||||
// px 값은 직접 사용, fr 값은 마이그레이션 호환
|
// px 값 -> minmax(Npx, auto): 최소 높이 보장 + 컨텐츠에 맞게 확장
|
||||||
return h.endsWith("px")
|
if (h.endsWith("px")) {
|
||||||
? h
|
return `minmax(${h}, auto)`;
|
||||||
: `${Math.round(parseFloat(h) * 32) || 32}px`;
|
}
|
||||||
|
// fr 값 -> 마이그레이션 호환: px 변환 후 minmax 적용
|
||||||
|
const px = Math.round(parseFloat(h) * 32) || 32;
|
||||||
|
return `minmax(${px}px, auto)`;
|
||||||
})
|
})
|
||||||
.join(" ")
|
.join(" ")
|
||||||
: `repeat(${Number(cardGrid.rows) || 1}, 32px)`,
|
: `repeat(${Number(cardGrid.rows) || 1}, minmax(32px, auto))`,
|
||||||
gap: `${Number(cardGrid.gap) || 0}px`,
|
gap: `${Number(cardGrid.gap) || 0}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -358,7 +461,7 @@ function renderCellContent(cell: CardCellDefinition, row: RowData): React.ReactN
|
||||||
<img
|
<img
|
||||||
src={displayValue}
|
src={displayValue}
|
||||||
alt={cell.label || cell.columnName}
|
alt={cell.label || cell.columnName}
|
||||||
className="h-full w-full object-cover rounded"
|
className="h-full max-h-[200px] w-full object-cover rounded"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center bg-muted rounded">
|
<div className="flex h-full items-center justify-center bg-muted rounded">
|
||||||
|
|
|
||||||
|
|
@ -219,13 +219,18 @@ export function PopStringListConfigPanel({ config, onUpdate }: ConfigPanelProps)
|
||||||
onColumnsChange={(cols) => {
|
onColumnsChange={(cols) => {
|
||||||
setSelectedColumns(cols);
|
setSelectedColumns(cols);
|
||||||
if (cfg.displayMode === "list") {
|
if (cfg.displayMode === "list") {
|
||||||
const listColumns: ListColumnConfig[] = cols.map((colName) => {
|
const currentList = cfg.listColumns || [];
|
||||||
const existing = cfg.listColumns?.find(
|
// 기존 리스트에서: 체크 해제된 메인 컬럼만 제거
|
||||||
(lc) => lc.columnName === colName
|
// 조인 컬럼 (이름에 "."이 포함)은 항상 보존
|
||||||
|
const preserved = currentList.filter(
|
||||||
|
(lc) => cols.includes(lc.columnName) || lc.columnName.includes(".")
|
||||||
);
|
);
|
||||||
return existing || { columnName: colName, label: colName };
|
// 새로 체크된 메인 컬럼만 리스트 끝에 추가
|
||||||
});
|
const existingNames = new Set(preserved.map((lc) => lc.columnName));
|
||||||
update({ selectedColumns: cols, listColumns });
|
const added = cols
|
||||||
|
.filter((colName) => !existingNames.has(colName))
|
||||||
|
.map((colName) => ({ columnName: colName, label: colName } as ListColumnConfig));
|
||||||
|
update({ selectedColumns: cols, listColumns: [...preserved, ...added] });
|
||||||
} else {
|
} else {
|
||||||
update({ selectedColumns: cols });
|
update({ selectedColumns: cols });
|
||||||
}
|
}
|
||||||
|
|
@ -237,7 +242,34 @@ export function PopStringListConfigPanel({ config, onUpdate }: ConfigPanelProps)
|
||||||
dataSource={cfg.dataSource}
|
dataSource={cfg.dataSource}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
mainColumns={columns}
|
mainColumns={columns}
|
||||||
onChange={(dataSource) => update({ dataSource })}
|
onChange={(dataSource) => {
|
||||||
|
// 조인 변경 후: 유효한 조인 컬럼명 셋 계산
|
||||||
|
const validJoinColNames = new Set(
|
||||||
|
(dataSource.joins || []).flatMap((j) =>
|
||||||
|
(j.selectedTargetColumns || []).map((col) => `${j.targetTable}.${col}`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// listColumns에서 고아 조인 컬럼 제거 + alternateColumns 정리
|
||||||
|
const currentList = cfg.listColumns || [];
|
||||||
|
const cleanedList = currentList
|
||||||
|
.filter((lc) => {
|
||||||
|
if (!lc.columnName.includes(".")) return true; // 메인 컬럼: 유지
|
||||||
|
return validJoinColNames.has(lc.columnName); // 조인 컬럼: 유효한 것만
|
||||||
|
})
|
||||||
|
.map((lc) => {
|
||||||
|
const alts = lc.alternateColumns;
|
||||||
|
if (!alts) return lc;
|
||||||
|
const cleanedAlts = alts.filter((a) => {
|
||||||
|
if (!a.includes(".")) return true; // 메인 컬럼: 유지
|
||||||
|
return validJoinColNames.has(a); // 조인 컬럼: 유효한 것만
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...lc,
|
||||||
|
alternateColumns: cleanedAlts.length > 0 ? cleanedAlts : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
update({ dataSource, listColumns: cleanedList });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{step === 5 &&
|
{step === 5 &&
|
||||||
|
|
@ -1077,6 +1109,65 @@ function StepListLayout({
|
||||||
// 컬럼 전환 설정 펼침 인덱스
|
// 컬럼 전환 설정 펼침 인덱스
|
||||||
const [expandedAltIdx, setExpandedAltIdx] = useState<number | null>(null);
|
const [expandedAltIdx, setExpandedAltIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// 리스트에 현재 포함된 컬럼명 셋
|
||||||
|
const listColumnNames = new Set(listColumns.map((c) => c.columnName));
|
||||||
|
|
||||||
|
// 추가 가능한 컬럼: (메인 + 조인) 중 현재 리스트에 없는 것
|
||||||
|
const addableColumns = [
|
||||||
|
...availableColumns
|
||||||
|
.filter((c) => !listColumnNames.has(c.name))
|
||||||
|
.map((c) => ({ value: c.name, label: c.name, source: "main" as const })),
|
||||||
|
...joinedColumns
|
||||||
|
.filter((c) => !listColumnNames.has(c.name))
|
||||||
|
.map((c) => ({
|
||||||
|
value: c.name,
|
||||||
|
label: `${c.displayName} (${c.sourceTable})`,
|
||||||
|
source: "join" as const,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 컬럼 추가 (독립 헤더로 추가 시 다른 컬럼의 alternateColumns에서 제거)
|
||||||
|
const addColumn = (columnValue: string) => {
|
||||||
|
const joinCol = joinedColumns.find((c) => c.name === columnValue);
|
||||||
|
const newCol: ListColumnConfig = {
|
||||||
|
columnName: columnValue,
|
||||||
|
label: joinCol?.displayName || columnValue,
|
||||||
|
};
|
||||||
|
// 다른 컬럼의 alternateColumns에서 이 컬럼 제거 (독립 헤더가 되므로)
|
||||||
|
const cleaned = listColumns.map((col) => {
|
||||||
|
const alts = col.alternateColumns;
|
||||||
|
if (!alts || !alts.includes(columnValue)) return col;
|
||||||
|
const newAlts = alts.filter((a) => a !== columnValue);
|
||||||
|
return { ...col, alternateColumns: newAlts.length > 0 ? newAlts : undefined };
|
||||||
|
});
|
||||||
|
onChange([...cleaned, newCol]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컬럼 삭제 (리스트에서만 삭제, STEP 3 체크 유지)
|
||||||
|
const removeColumn = (index: number) => {
|
||||||
|
const next = listColumns.filter((_, i) => i !== index);
|
||||||
|
onChange(next);
|
||||||
|
// 펼침 인덱스 초기화 (삭제로 인덱스가 밀리므로)
|
||||||
|
setExpandedAltIdx(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 전환 후보: (메인 + 조인) - 자기 자신 - 리스트에 독립 헤더로 있는 것
|
||||||
|
const getAlternateCandidates = (currentColumnName: string) => {
|
||||||
|
return [
|
||||||
|
...availableColumns
|
||||||
|
.filter((c) => c.name !== currentColumnName && !listColumnNames.has(c.name))
|
||||||
|
.map((c) => ({ value: c.name, label: c.name, source: "main" as const })),
|
||||||
|
...joinedColumns
|
||||||
|
.filter((c) => c.name !== currentColumnName && !listColumnNames.has(c.name))
|
||||||
|
.map((c) => ({
|
||||||
|
value: c.name,
|
||||||
|
label: c.displayName,
|
||||||
|
source: "join" as const,
|
||||||
|
sourceTable: c.sourceTable,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
const updateColumn = (index: number, partial: Partial<ListColumnConfig>) => {
|
const updateColumn = (index: number, partial: Partial<ListColumnConfig>) => {
|
||||||
const next = listColumns.map((col, i) =>
|
const next = listColumns.map((col, i) =>
|
||||||
i === index ? { ...col, ...partial } : col
|
i === index ? { ...col, ...partial } : col
|
||||||
|
|
@ -1163,7 +1254,7 @@ function StepListLayout({
|
||||||
setDraggableRow(null);
|
setDraggableRow(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (listColumns.length === 0) {
|
if (listColumns.length === 0 && addableColumns.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
컬럼을 먼저 선택하세요
|
컬럼을 먼저 선택하세요
|
||||||
|
|
@ -1309,8 +1400,8 @@ function StepListLayout({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* 컬럼 전환 버튼 (조인 컬럼 있을 때만) */}
|
{/* 컬럼 전환 버튼 (전환 후보가 있을 때만) */}
|
||||||
{joinedColumns.length > 0 && (
|
{getAlternateCandidates(col.columnName).length > 0 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
@ -1327,23 +1418,36 @@ function StepListLayout({
|
||||||
<ChevronsUpDown className="h-3 w-3" />
|
<ChevronsUpDown className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 컬럼 삭제 버튼 */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeColumn(i)}
|
||||||
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||||
|
title="컬럼 삭제"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 전환 가능 컬럼 (펼침 시만 표시, 조인 컬럼만) */}
|
{/* 전환 가능 컬럼 (펼침 시만 표시, 메인+조인 중 리스트 미포함분) */}
|
||||||
{expandedAltIdx === i && joinedColumns.length > 0 && (
|
{expandedAltIdx === i && (() => {
|
||||||
<div className="ml-5 flex items-center gap-1 pb-1">
|
const candidates = getAlternateCandidates(col.columnName);
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className="ml-5 flex flex-wrap items-center gap-1 pb-1">
|
||||||
<span className="shrink-0 text-[8px] text-muted-foreground">전환:</span>
|
<span className="shrink-0 text-[8px] text-muted-foreground">전환:</span>
|
||||||
{joinedColumns.map((jc) => {
|
{candidates.map((cand) => {
|
||||||
const alts = col.alternateColumns || [];
|
const alts = col.alternateColumns || [];
|
||||||
const isAlt = alts.includes(jc.name);
|
const isAlt = alts.includes(cand.value);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={jc.name}
|
key={cand.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newAlts = isAlt
|
const newAlts = isAlt
|
||||||
? alts.filter((a) => a !== jc.name)
|
? alts.filter((a) => a !== cand.value)
|
||||||
: [...alts, jc.name];
|
: [...alts, cand.value];
|
||||||
updateColumn(i, {
|
updateColumn(i, {
|
||||||
alternateColumns: newAlts.length > 0 ? newAlts : undefined,
|
alternateColumns: newAlts.length > 0 ? newAlts : undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -1355,16 +1459,59 @@ function StepListLayout({
|
||||||
: "border-border hover:bg-muted"
|
: "border-border hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{jc.displayName}
|
{cand.label}
|
||||||
|
{cand.source === "join" && (
|
||||||
|
<span className="ml-0.5 text-[7px] text-muted-foreground">*</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 컬럼 추가 */}
|
||||||
|
{addableColumns.length > 0 && (
|
||||||
|
<Select onValueChange={addColumn}>
|
||||||
|
<SelectTrigger className="h-7 w-full text-[10px]">
|
||||||
|
<SelectValue placeholder="+ 컬럼 추가..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{addableColumns.some((c) => c.source === "main") && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="__main_header__" disabled className="text-[8px] font-medium text-muted-foreground">
|
||||||
|
메인 테이블
|
||||||
|
</SelectItem>
|
||||||
|
{addableColumns
|
||||||
|
.filter((c) => c.source === "main")
|
||||||
|
.map((c) => (
|
||||||
|
<SelectItem key={c.value} value={c.value} className="text-[10px]">
|
||||||
|
{c.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{addableColumns.some((c) => c.source === "join") && (
|
||||||
|
<>
|
||||||
|
<SelectItem value="__join_header__" disabled className="text-[8px] font-medium text-muted-foreground">
|
||||||
|
조인 테이블
|
||||||
|
</SelectItem>
|
||||||
|
{addableColumns
|
||||||
|
.filter((c) => c.source === "join")
|
||||||
|
.map((c) => (
|
||||||
|
<SelectItem key={c.value} value={c.value} className="text-[10px]">
|
||||||
|
{c.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-[8px] text-muted-foreground">
|
<p className="text-[8px] text-muted-foreground">
|
||||||
행을 드래그하여 순서 변경 | 상단 바 경계를 드래그하여 너비 조정
|
행을 드래그하여 순서 변경 | 상단 바 경계를 드래그하여 너비 조정
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue