- {/* 계산 모드 */}
-
-
-
-
-
- {/* 계산 결과 필드 */}
-
-
-
-
-
- {/* 템플릿 모드 필드 매핑 */}
- {config.autoCalculation.mode === "template" && (
-
-
- 필드 매핑
-
- {(
- [
- ["basePrice", "기준 단가"],
- ["discountType", "할인 방식"],
- ["discountValue", "할인값"],
- ["roundingType", "반올림 방식"],
- ["roundingUnit", "반올림 단위"],
- ] as const
- ).map(([key, label]) => (
-
-
- {label}
-
+ {config.autoCalculation && (
+
+
+
+
+
+
+
+
- ))}
-
- )}
-
- )}
-
-
- {/* ─── 고급 설정 (Collapsible) ─── */}
-
toggleSection("advanced")}
- >
-
-
-
- 고급 설정
-
-
-
-
- {/* sourceKeyField */}
-
-
-
handleChange("sourceKeyField", name)}
- />
-
- 대상 테이블에서 원본을 참조하는 FK 컬럼
-
-
+ {config.autoCalculation.mode === "template" && (
+
+
필드 매핑
+ {(
+ [
+ ["basePrice", "기준 단가"],
+ ["discountType", "할인 방식"],
+ ["discountValue", "할인값"],
+ ["roundingType", "반올림 방식"],
+ ["roundingUnit", "반올림 단위"],
+ ] as const
+ ).map(([key, label]) => (
+
+ {label}
+
+
+ ))}
+
+ )}
+
+ )}
+
+
- {/* 항목 번호 표시 */}
-
-
- handleChange("showIndex", v)}
- />
-
-
- {/* 항목 삭제 허용 */}
-
-
- handleChange("allowRemove", v)}
- />
-
-
- {/* 비활성화 */}
-
-
- handleChange("disabled", v)}
- />
-
-
- {/* 읽기 전용 */}
-
-
- handleChange("readonly", v)}
- />
-
-
- {/* 빈 상태 메시지 */}
-
-
-
- handleChange("emptyMessage", e.target.value)
- }
- placeholder="전달받은 데이터가 없습니다."
- className="h-8 text-xs"
- />
diff --git a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx
index 489cd1dc..7a65741e 100644
--- a/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx
+++ b/frontend/components/v2/config-panels/V2TableListConfigPanel.tsx
@@ -49,7 +49,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove }
import { CSS } from "@dnd-kit/utilities";
import type { TableListConfig, ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
-// ─── DnD 정렬 가능한 컬럼 행 ───
+// ─── DnD 정렬 가능한 컬럼 행 (접이식) ───
function SortableColumnRow({
id,
col,
@@ -69,40 +69,57 @@ function SortableColumnRow({
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition };
+ const [expanded, setExpanded] = useState(false);
return (
-
-
+
+
+
+
+ {isEntityJoin ? (
+
+ ) : (
+
#{index + 1}
+ )}
+
+ {col.width && (
+
{col.width}px
+ )}
+
- {isEntityJoin ? (
-
- ) : (
-
#{index + 1}
+ {expanded && (
+
+ onLabelChange(e.target.value)}
+ placeholder="표시명"
+ className="h-7 min-w-0 text-xs"
+ />
+ onWidthChange(parseInt(e.target.value) || 100)}
+ placeholder="너비"
+ className="h-7 shrink-0 text-xs text-center"
+ />
+
)}
-
onLabelChange(e.target.value)}
- placeholder="표시명"
- className="h-6 min-w-0 flex-1 text-xs"
- />
-
onWidthChange(parseInt(e.target.value) || 100)}
- placeholder="너비"
- className="h-6 w-14 shrink-0 text-xs"
- />
-
);
}
@@ -230,6 +247,11 @@ export const V2TableListConfigPanel: React.FC
= ({
// Collapsible 상태
const [advancedOpen, setAdvancedOpen] = useState(false);
const [entityDisplayOpen, setEntityDisplayOpen] = useState(false);
+ const [columnSelectOpen, setColumnSelectOpen] = useState(() => (config.columns?.length || 0) > 0);
+ const [entityJoinOpen, setEntityJoinOpen] = useState(false);
+ const [displayColumnsOpen, setDisplayColumnsOpen] = useState(() => (config.columns?.length || 0) > 0);
+ const [columnSearchText, setColumnSearchText] = useState("");
+ const [entityJoinSubOpen, setEntityJoinSubOpen] = useState>({});
// 이전 컬럼 개수 추적 (엔티티 감지용)
const prevColumnsLengthRef = useRef(0);
@@ -740,149 +762,215 @@ export const V2TableListConfigPanel: React.FC = ({
{/* ═══════════════════════════════════════ */}
- {/* 2단계: 컬럼 선택 */}
+ {/* 2단계: 컬럼 선택 (Collapsible) */}
{/* ═══════════════════════════════════════ */}
{targetTableName && availableColumns.length > 0 && (
<>
-
-
-
-
-
- {availableColumns.map((column) => {
- const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
- return (
-
{
- if (isAdded) {
- updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
- } else {
- addColumn(column.columnName);
- }
- }}
- >
- {
- if (isAdded) {
- updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
- } else {
- addColumn(column.columnName);
- }
- }}
- className="pointer-events-none h-3.5 w-3.5"
- />
-
- {column.label || column.columnName}
- {isAdded && (
-
- )}
-
- {column.input_type || column.dataType}
-
-
- );
- })}
-
-
-
- {/* Entity 조인 컬럼 */}
- {entityJoinColumns.joinTables.length > 0 && (
-
-
-
-
- {entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
-
-
-
- {joinTable.tableName}
-
- {joinTable.currentDisplayColumn}
-
-
-
- {joinTable.availableColumns.map((column, colIndex) => {
- const matchingJoinColumn = entityJoinColumns.availableColumns.find(
- (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
- );
- const isAlreadyAdded = config.columns?.some(
- (col) => col.columnName === matchingJoinColumn?.joinAlias,
- );
- if (!matchingJoinColumn) return null;
-
- return (
-
{
- if (isAlreadyAdded) {
- updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
+
+
+
+
+
+
+
setColumnSearchText(e.target.value)}
+ placeholder="컬럼 검색..."
+ className="h-7 text-xs"
+ />
+
+ {availableColumns
+ .filter((column) => {
+ if (!columnSearchText) return true;
+ const search = columnSearchText.toLowerCase();
+ return (
+ column.columnName.toLowerCase().includes(search) ||
+ (column.label || "").toLowerCase().includes(search)
+ );
+ })
+ .map((column) => {
+ const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
+ return (
+
{
+ if (isAdded) {
+ updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
+ } else {
+ addColumn(column.columnName);
+ }
+ }}
+ >
+ {
+ if (isAdded) {
+ updateField("columns", config.columns?.filter((c) => c.columnName !== column.columnName) || []);
} else {
- addEntityColumn(matchingJoinColumn);
+ addColumn(column.columnName);
}
}}
- >
- {
- if (isAlreadyAdded) {
- updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
- } else {
- addEntityColumn(matchingJoinColumn);
+ className="pointer-events-none h-3.5 w-3.5"
+ />
+
+ {column.label || column.columnName}
+ {isAdded && (
+
- );
- })}
-
-
- ))}
+ >
+ {config.columns?.find((c) => c.columnName === column.columnName)?.editable === false ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {column.input_type || column.dataType}
+
+
+ );
+ })}
+
-
+
+
+
+ {/* Entity 조인 컬럼 (Collapsible) */}
+ {entityJoinColumns.joinTables.length > 0 && (
+
+
+
+
+
+
+ {entityJoinColumns.joinTables.map((joinTable, tableIndex) => {
+ const addedCount = joinTable.availableColumns.filter((col) => {
+ const match = entityJoinColumns.availableColumns.find(
+ (jc) => jc.tableName === joinTable.tableName && jc.columnName === col.columnName,
+ );
+ return match && config.columns?.some((c) => c.columnName === match.joinAlias);
+ }).length;
+ const isSubOpen = entityJoinSubOpen[tableIndex] ?? false;
+
+ return (
+
setEntityJoinSubOpen((prev) => ({ ...prev, [tableIndex]: open }))}>
+
+
+
+
+
+ {joinTable.availableColumns.map((column, colIndex) => {
+ const matchingJoinColumn = entityJoinColumns.availableColumns.find(
+ (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
+ );
+ const isAlreadyAdded = config.columns?.some(
+ (col) => col.columnName === matchingJoinColumn?.joinAlias,
+ );
+ if (!matchingJoinColumn) return null;
+
+ return (
+
{
+ if (isAlreadyAdded) {
+ updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
+ } else {
+ addEntityColumn(matchingJoinColumn);
+ }
+ }}
+ >
+ {
+ if (isAlreadyAdded) {
+ updateField("columns", config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || []);
+ } else {
+ addEntityColumn(matchingJoinColumn);
+ }
+ }}
+ className="pointer-events-none h-3.5 w-3.5"
+ />
+
+ {column.columnLabel}
+
+ {column.inputType || column.dataType}
+
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+
)}
>
)}
@@ -905,59 +993,73 @@ export const V2TableListConfigPanel: React.FC
= ({
)}
{/* ═══════════════════════════════════════ */}
- {/* 3단계: 선택된 컬럼 순서 (DnD) */}
+ {/* 3단계: 표시할 컬럼 (Collapsible + DnD) */}
{/* ═══════════════════════════════════════ */}
{config.columns && config.columns.length > 0 && (
-
-
-
-
{
- const { active, over } = event;
- if (!over || active.id === over.id) return;
- const columns = [...(config.columns || [])];
- const oldIndex = columns.findIndex((c) => c.columnName === active.id);
- const newIndex = columns.findIndex((c) => c.columnName === over.id);
- if (oldIndex !== -1 && newIndex !== -1) {
- const reordered = arrayMove(columns, oldIndex, newIndex);
- reordered.forEach((col, idx) => { col.order = idx; });
- updateField("columns", reordered);
- }
- }}
- >
- c.columnName)}
- strategy={verticalListSortingStrategy}
+
+
+
+
+
+
+
드래그하여 순서 변경, 클릭하여 표시명/너비 수정
+
{
+ const { active, over } = event;
+ if (!over || active.id === over.id) return;
+ const columns = [...(config.columns || [])];
+ const oldIndex = columns.findIndex((c) => c.columnName === active.id);
+ const newIndex = columns.findIndex((c) => c.columnName === over.id);
+ if (oldIndex !== -1 && newIndex !== -1) {
+ const reordered = arrayMove(columns, oldIndex, newIndex);
+ reordered.forEach((col, idx) => { col.order = idx; });
+ updateField("columns", reordered);
+ }
+ }}
+ >
+ c.columnName)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {(config.columns || []).map((column, idx) => {
+ const resolvedLabel =
+ column.displayName && column.displayName !== column.columnName
+ ? column.displayName
+ : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
+ const colWithLabel = { ...column, displayName: resolvedLabel };
+ return (
+ updateColumn(column.columnName, { displayName: value })}
+ onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
+ onRemove={() => removeColumn(column.columnName)}
+ />
+ );
+ })}
+
+
+
+
+
+
)}
{/* ═══════════════════════════════════════ */}