새로운 문서 파일을 추가하여 현재 사용 가능한 16개 컴포넌트의 다국어 지원 및 테이블 설정 기능 현황을 정리했습니다. 각 컴포넌트별 상세 현황과 우선순위 작업 목록을 포함하여 기능 적용 상태를 명확히 하였습니다.

This commit is contained in:
kjs 2026-01-15 17:00:21 +09:00
parent e937ba9161
commit 57d86c8ef1
3 changed files with 293 additions and 483 deletions

View File

@ -0,0 +1,148 @@
# 컴포넌트 기능 현황
> 작성일: 2026-01-15
> 현재 사용 가능한 16개 컴포넌트의 다국어 지원 및 테이블 설정 기능 현황
---
## 요약
| 기능 | 적용 완료 | 미적용 | 해당없음 |
|------|----------|--------|---------|
| **다국어 지원** | 3개 | 10개 | 3개 |
| **컴포넌트별 테이블 설정** | 4개 | 6개 | 6개 |
---
## 컴포넌트별 상세 현황
### 데이터 표시 (Display) - 4개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|---------|:----------:|:----------:|------|
| **테이블 리스트** | ✅ 적용 | ✅ 적용 | `customTableName`, `useCustomTable` 지원 |
| **카드 디스플레이** | ❌ 미적용 | ⚠️ 부분 | `screenTableName`만 사용, 컴포넌트별 테이블 선택 UI 없음 |
| **텍스트 표시** | ❌ 미적용 | 해당없음 | 정적 텍스트 표시용 |
| **피벗 그리드** | ❌ 미적용 | ⚠️ 부분 | `tableName` 설정 가능하나 Combobox UI 없음 |
---
### 데이터 입력 (Data) - 2개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|---------|:----------:|:----------:|------|
| **통합 반복 데이터** | ❌ 미적용 | ✅ 적용 | `mainTableName`, `foreignKeyColumn` 지원, Combobox UI 적용 |
| **반복 화면 모달** | ❌ 미적용 | ⚠️ 부분 | `tableName` 설정 가능하나 Combobox UI 없음 |
---
### 액션 (Action) - 1개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|---------|:----------:|:----------:|------|
| **기본 버튼** | ✅ 적용 | 해당없음 | `langKeyId`, `langKey` 지원 |
---
### 레이아웃 (Layout) - 5개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|---------|:----------:|:----------:|------|
| **분할 패널** | ✅ 적용 | ⚠️ 부분 | 다국어 지원, 테이블 설정은 하위 패널에서 처리 |
| **탭 컴포넌트** | ❌ 미적용 | 해당없음 | 화면 전환용 컨테이너 |
| **Section Card** | ❌ 미적용 | 해당없음 | 그룹화 컨테이너 |
| **Section Paper** | ❌ 미적용 | 해당없음 | 그룹화 컨테이너 |
| **구분선** | ❌ 미적용 | 해당없음 | 시각적 구분용 |
---
### 유틸리티 (Utility) - 4개
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|---------|:----------:|:----------:|------|
| **코드 채번 규칙** | ❌ 미적용 | 해당없음 | 채번 규칙 관리 전용 |
| **렉 구조 설정** | ❌ 미적용 | 해당없음 | 창고 렉 설정 전용 |
| **출발지/도착지 선택** | ❌ 미적용 | ⚠️ 부분 | `customTableName` 지원하나 Combobox UI 없음 |
| **검색 필터** | ❌ 미적용 | ⚠️ 부분 | `screenTableName` 자동 감지 |
---
## 상세 설명
### 다국어 지원 (`langKeyId`, `langKey`)
다국어 지원이란 컴포넌트의 라벨, 플레이스홀더 등 텍스트 속성에 다국어 키를 연결하여 언어별로 다른 텍스트를 표시하는 기능입니다.
**적용 완료 (3개)**
- `table-list`: 컬럼 라벨 다국어 지원
- `button-primary`: 버튼 텍스트 다국어 지원
- `split-panel-layout`: 패널 제목 다국어 지원
**미적용 (10개)**
- `card-display`, `text-display`, `pivot-grid`
- `unified-repeater`, `repeat-screen-modal`
- `tabs`, `section-card`, `section-paper`, `divider-line`
- `numbering-rule`, `rack-structure`, `location-swap-selector`, `table-search-widget`
---
### 컴포넌트별 테이블 설정 (`customTableName`, `useCustomTable`)
컴포넌트별 테이블 설정이란 화면의 메인 테이블과 별개로 컴포넌트가 자체적으로 사용할 테이블을 지정할 수 있는 기능입니다.
**완전 적용 (4개)**
| 컴포넌트 | 적용 방식 |
|---------|----------|
| `table-list` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable`, `isReadOnly` 지원 |
| `unified-repeater` | Combobox UI로 테이블 선택, `mainTableName`, `foreignKeyColumn` 지원, FK 자동 연결 |
| `unified-list` | `TableListConfigPanel` 래핑하여 동일 기능 제공 |
**부분 적용 (6개)**
| 컴포넌트 | 현재 상태 | 필요 작업 |
|---------|----------|----------|
| `card-display` | `screenTableName` 사용 | Combobox UI 추가 필요 |
| `pivot-grid` | `tableName` 설정 가능 | Combobox UI 추가 필요 |
| `repeat-screen-modal` | `tableName` 설정 가능 | Combobox UI 추가 필요 |
| `split-panel-layout` | 하위 패널에서 처리 | 하위 컴포넌트에 위임 |
| `location-swap-selector` | `customTableName` 지원 | Combobox UI 추가 필요 |
| `table-search-widget` | `screenTableName` 자동 감지 | 현재 방식 유지 가능 |
**해당없음 (6개)**
- `text-display`, `divider-line`: 정적 컴포넌트
- `tabs`, `section-card`, `section-paper`: 레이아웃 컨테이너
- `numbering-rule`, `rack-structure`: 특수 목적 컴포넌트
---
## 우선순위 작업 목록
### 1순위: 데이터 컴포넌트 테이블 설정 UI 통일
| 컴포넌트 | 작업 내용 |
|---------|----------|
| `card-display` | Combobox UI 추가, `customTableName` 지원 |
| `pivot-grid` | Combobox UI 추가 |
| `repeat-screen-modal` | Combobox UI 추가 |
### 2순위: 다국어 지원 확대
| 컴포넌트 | 작업 내용 |
|---------|----------|
| `unified-repeater` | 컬럼 라벨 `langKeyId` 지원 |
| `card-display` | 필드 라벨 `langKeyId` 지원 |
| `tabs` | 탭 이름 `langKeyId` 지원 |
| `section-card` | 제목 `langKeyId` 지원 |
---
## 범례
| 기호 | 의미 |
|-----|------|
| ✅ | 완전 적용 |
| ⚠️ | 부분 적용 (기능은 있으나 UI 미비) |
| ❌ | 미적용 |
| | 해당없음 (기능 불필요) |

View File

@ -66,14 +66,7 @@ export function ComponentsPanel({
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
{
id: "unified-list",
name: "통합 목록",
description: "테이블, 카드 등 다양한 데이터 표시 방식 지원",
category: "display" as ComponentCategory,
tags: ["table", "list", "card", "unified"],
defaultSize: { width: 600, height: 400 },
},
// unified-list: table-list, card-display로 분리하여 숨김 처리
// unified-media 제거 - 테이블 컬럼의 image/file 입력 타입으로 사용
// unified-biz 제거 - 개별 컴포넌트(flow-widget, rack-structure, numbering-rule)로 직접 표시
// unified-hierarchy 제거 - 현재 미사용
@ -113,8 +106,7 @@ export function ComponentsPanel({
// 특수 업무용 컴포넌트 (일반 화면에서 불필요)
"tax-invoice-list", // 세금계산서 전용
"customer-item-mapping", // 고객-품목 매핑 전용
// unified-list로 통합됨
"card-display", // → unified-list (card 모드)
// card-display는 별도 컴포넌트로 유지
// unified-media로 통합됨
"image-display", // → unified-media (image)
// 공통코드관리로 통합 예정
@ -128,6 +120,12 @@ export function ComponentsPanel({
"universal-form-modal", // 범용 폼 모달
// 통합 미디어 (테이블 컬럼 입력 타입으로 사용)
"unified-media", // → 테이블 컬럼의 image/file 입력 타입으로 사용
// 플로우 위젯 숨김 처리
"flow-widget",
// 선택 항목 상세입력 - 기존 컴포넌트 조합으로 대체 가능
"selected-items-detail-input",
// 연관 데이터 버튼 - unified-repeater로 대체 가능
"related-data-buttons",
];
return {

View File

@ -2,21 +2,13 @@
/**
* UnifiedList
* .
* -
* - +
* TableListConfigPanel을 .
* card-display .
*/
import React, { useState, useEffect, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Database, Link2, GripVertical, ChevronDown, ChevronRight } from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
import { cn } from "@/lib/utils";
import React, { useMemo } from "react";
import { TableListConfigPanel } from "@/lib/registry/components/table-list/TableListConfigPanel";
import { TableListConfig } from "@/lib/registry/components/table-list/types";
interface UnifiedListConfigPanelProps {
config: Record<string, any>;
@ -25,476 +17,148 @@ interface UnifiedListConfigPanelProps {
currentTableName?: string;
}
interface ColumnOption {
columnName: string;
displayName: string;
isJoinColumn?: boolean;
sourceTable?: string;
inputType?: string;
}
/**
* UnifiedList
* TableListConfigPanel과
*/
export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
config,
onChange,
currentTableName,
}) => {
// 컬럼 목록 (테이블 컬럼 + 엔티티 조인 컬럼)
const [columns, setColumns] = useState<ColumnOption[]>([]);
const [loadingColumns, setLoadingColumns] = useState(false);
const [expandedJoinSections, setExpandedJoinSections] = useState<Set<string>>(new Set());
// UnifiedList config를 TableListConfig 형식으로 변환
const tableListConfig: TableListConfig = useMemo(() => {
// 컬럼 형식 변환: UnifiedList columns -> TableList columns
const columns = (config.columns || []).map((col: any, index: number) => ({
columnName: col.key || col.columnName || col.field || "",
displayName: col.title || col.header || col.displayName || col.key || col.columnName || col.field || "",
width: col.width ? parseInt(col.width, 10) : undefined,
visible: col.visible !== false,
sortable: col.sortable !== false,
searchable: col.searchable !== false,
align: col.align || "left",
order: index,
isEntityJoin: col.isJoinColumn || col.isEntityJoin || false,
thousandSeparator: col.thousandSeparator,
editable: col.editable,
entityDisplayConfig: col.entityDisplayConfig,
}));
// 설정 업데이트 핸들러
const updateConfig = (field: string, value: any) => {
const newConfig = { ...config, [field]: value };
console.log("⚙️ UnifiedListConfigPanel updateConfig:", { field, value, newConfig });
return {
selectedTable: config.tableName || config.dataSource?.table || currentTableName,
tableName: config.tableName || config.dataSource?.table || currentTableName,
columns,
useCustomTable: config.useCustomTable,
customTableName: config.customTableName,
isReadOnly: config.isReadOnly !== false, // UnifiedList는 기본적으로 읽기 전용
displayMode: "table", // 테이블 모드 고정 (카드는 card-display 컴포넌트 사용)
pagination: config.pagination !== false ? {
enabled: true,
pageSize: config.pageSize || 10,
position: "bottom",
showPageSize: true,
pageSizeOptions: [5, 10, 20, 50, 100],
} : {
enabled: false,
pageSize: 10,
position: "bottom",
showPageSize: false,
pageSizeOptions: [10],
},
filter: config.filter,
dataFilter: config.dataFilter,
checkbox: {
enabled: true,
position: "left",
showHeader: true,
},
height: "auto",
autoWidth: true,
stickyHeader: true,
autoLoad: true,
horizontalScroll: {
enabled: true,
minColumnWidth: 100,
maxColumnWidth: 300,
},
};
}, [config, currentTableName]);
// TableListConfig 변경을 UnifiedList config 형식으로 변환
const handleConfigChange = (partialConfig: Partial<TableListConfig>) => {
const newConfig: Record<string, any> = { ...config };
// 테이블 설정 변환
if (partialConfig.selectedTable !== undefined) {
newConfig.tableName = partialConfig.selectedTable;
if (!newConfig.dataSource) {
newConfig.dataSource = {};
}
newConfig.dataSource.table = partialConfig.selectedTable;
}
if (partialConfig.tableName !== undefined) {
newConfig.tableName = partialConfig.tableName;
if (!newConfig.dataSource) {
newConfig.dataSource = {};
}
newConfig.dataSource.table = partialConfig.tableName;
}
if (partialConfig.useCustomTable !== undefined) {
newConfig.useCustomTable = partialConfig.useCustomTable;
}
if (partialConfig.customTableName !== undefined) {
newConfig.customTableName = partialConfig.customTableName;
}
if (partialConfig.isReadOnly !== undefined) {
newConfig.isReadOnly = partialConfig.isReadOnly;
}
// 컬럼 형식 변환: TableList columns -> UnifiedList columns
if (partialConfig.columns !== undefined) {
newConfig.columns = partialConfig.columns.map((col: any) => ({
key: col.columnName,
field: col.columnName,
title: col.displayName,
header: col.displayName,
width: col.width ? String(col.width) : undefined,
visible: col.visible,
sortable: col.sortable,
searchable: col.searchable,
align: col.align,
isJoinColumn: col.isEntityJoin,
isEntityJoin: col.isEntityJoin,
thousandSeparator: col.thousandSeparator,
editable: col.editable,
entityDisplayConfig: col.entityDisplayConfig,
}));
}
// 페이지네이션 변환
if (partialConfig.pagination !== undefined) {
newConfig.pagination = partialConfig.pagination?.enabled;
newConfig.pageSize = partialConfig.pagination?.pageSize || 10;
}
// 필터 변환
if (partialConfig.filter !== undefined) {
newConfig.filter = partialConfig.filter;
}
// 데이터 필터 변환
if (partialConfig.dataFilter !== undefined) {
newConfig.dataFilter = partialConfig.dataFilter;
}
console.log("⚙️ UnifiedListConfigPanel handleConfigChange:", { partialConfig, newConfig });
onChange(newConfig);
};
// 테이블명 (현재 화면의 테이블 사용)
const tableName = currentTableName || config.tableName;
// 화면의 테이블명을 config에 자동 저장
useEffect(() => {
if (currentTableName && config.tableName !== currentTableName) {
onChange({ ...config, tableName: currentTableName });
}
}, [currentTableName]);
// 테이블 컬럼 및 엔티티 조인 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!tableName) {
setColumns([]);
return;
}
setLoadingColumns(true);
try {
// 1. 테이블 컬럼 로드
const columnData = await tableTypeApi.getColumns(tableName);
const baseColumns: ColumnOption[] = columnData.map((c: any) => ({
columnName: c.columnName || c.column_name,
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
isJoinColumn: false,
inputType: c.inputType || c.input_type || c.webType || c.web_type,
}));
// 2. 엔티티 타입 컬럼 찾기 및 조인 컬럼 정보 로드
const entityColumns = columnData.filter((c: any) => (c.inputType || c.input_type) === "entity");
const joinColumnOptions: ColumnOption[] = [];
for (const entityCol of entityColumns) {
const colName = entityCol.columnName || entityCol.column_name;
// referenceTable 우선순위:
// 1. 컬럼의 reference_table 필드
// 2. detailSettings.referenceTable
let referenceTable = entityCol.referenceTable || entityCol.reference_table;
if (!referenceTable) {
let detailSettings = entityCol.detailSettings || entityCol.detail_settings;
if (typeof detailSettings === "string") {
try {
detailSettings = JSON.parse(detailSettings);
} catch {
continue;
}
}
referenceTable = detailSettings?.referenceTable;
}
if (referenceTable) {
try {
const refColumnData = await tableTypeApi.getColumns(referenceTable);
refColumnData.forEach((refCol: any) => {
const refColName = refCol.columnName || refCol.column_name;
const refDisplayName = refCol.displayName || refCol.columnLabel || refColName;
joinColumnOptions.push({
columnName: `${colName}.${refColName}`,
displayName: refDisplayName,
isJoinColumn: true,
sourceTable: referenceTable,
});
});
} catch (error) {
console.error(`참조 테이블 ${referenceTable} 컬럼 로드 실패:`, error);
}
}
}
setColumns([...baseColumns, ...joinColumnOptions]);
} catch (error) {
console.error("컬럼 목록 로드 실패:", error);
setColumns([]);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
}, [tableName]);
// 컬럼 설정
const configColumns: Array<{
key: string;
title: string;
width?: string;
isJoinColumn?: boolean;
inputType?: string;
thousandSeparator?: boolean;
}> = config.columns || [];
// 컬럼이 추가되었는지 확인
const isColumnAdded = (columnName: string) => {
return configColumns.some((col) => col.key === columnName);
};
// 컬럼 토글 (추가/제거)
const toggleColumn = (column: ColumnOption) => {
if (isColumnAdded(column.columnName)) {
// 제거
const newColumns = configColumns.filter((col) => col.key !== column.columnName);
updateConfig("columns", newColumns);
} else {
// 추가
const isNumberType = ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(
column.inputType || "",
);
const newColumn = {
key: column.columnName,
title: column.displayName,
width: "",
isJoinColumn: column.isJoinColumn || false,
inputType: column.inputType,
thousandSeparator: isNumberType ? true : undefined, // 숫자 타입은 기본적으로 천단위 구분자 사용
};
updateConfig("columns", [...configColumns, newColumn]);
}
};
// 컬럼 천단위 구분자 토글
const toggleThousandSeparator = (columnKey: string, checked: boolean) => {
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, thousandSeparator: checked } : col));
updateConfig("columns", newColumns);
};
// 숫자 타입 컬럼인지 확인
const isNumberColumn = (columnKey: string) => {
const colInfo = columns.find((c) => c.columnName === columnKey);
const configCol = configColumns.find((c) => c.key === columnKey);
const inputType = configCol?.inputType || colInfo?.inputType || "";
return ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(inputType);
};
// 컬럼 제목 수정
const updateColumnTitle = (columnKey: string, title: string) => {
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, title } : col));
updateConfig("columns", newColumns);
};
// 그룹별 컬럼 분리
const baseColumns = useMemo(() => columns.filter((col) => !col.isJoinColumn), [columns]);
// 조인 컬럼을 소스 테이블별로 그룹화
const joinColumnsByTable = useMemo(() => {
const grouped: Record<string, ColumnOption[]> = {};
columns
.filter((col) => col.isJoinColumn)
.forEach((col) => {
const table = col.sourceTable || "unknown";
if (!grouped[table]) {
grouped[table] = [];
}
grouped[table].push(col);
});
return grouped;
}, [columns]);
// 조인 섹션 토글
const toggleJoinSection = (tableName: string) => {
setExpandedJoinSections((prev) => {
const newSet = new Set(prev);
if (newSet.has(tableName)) {
newSet.delete(tableName);
} else {
newSet.add(tableName);
}
return newSet;
});
};
return (
<div className="space-y-4">
{/* 뷰 모드 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select value={config.viewMode || "table"} onValueChange={(value) => updateConfig("viewMode", value)}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="방식 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="table"></SelectItem>
<SelectItem value="card"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 카드 모드 설정 */}
{config.viewMode === "card" && (
<>
<Separator />
<div className="space-y-3">
<Label className="text-xs font-medium"> </Label>
{/* 제목 컬럼 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
<Select
value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) => updateConfig("cardConfig", { ...config.cardConfig, titleColumn: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{configColumns.map((col: any) => (
<SelectItem key={col.key} value={col.key}>
{col.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 부제목 컬럼 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
<Select
value={config.cardConfig?.subtitleColumn || "_none_"}
onValueChange={(value) =>
updateConfig("cardConfig", { ...config.cardConfig, subtitleColumn: value === "_none_" ? "" : value })
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_none_"></SelectItem>
{configColumns.map((col: any) => (
<SelectItem key={col.key} value={col.key}>
{col.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 행당 카드 수 */}
<div className="space-y-1">
<Label className="text-muted-foreground text-[10px]"> </Label>
<Select
value={String(config.cardConfig?.cardsPerRow || 3)}
onValueChange={(value) =>
updateConfig("cardConfig", { ...config.cardConfig, cardsPerRow: parseInt(value) })
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</>
)}
<Separator />
{/* 컬럼 선택 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>
) : !tableName ? (
<p className="text-muted-foreground py-2 text-xs"> </p>
) : (
<div className="max-h-60 space-y-2 overflow-y-auto rounded-md border p-2">
{/* 테이블 컬럼 */}
<div className="space-y-0.5">
{baseColumns.map((column) => (
<div
key={column.columnName}
className={cn(
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isColumnAdded(column.columnName) && "bg-primary/10",
)}
onClick={() => toggleColumn(column)}
>
<Checkbox
checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleColumn(column)}
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
/>
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<span className="truncate text-xs">{column.displayName}</span>
</div>
))}
</div>
{/* 조인 컬럼 (테이블별 그룹) */}
{Object.keys(joinColumnsByTable).length > 0 && (
<div className="mt-2 border-t pt-2">
<div className="text-muted-foreground mb-1 flex items-center gap-1 text-[10px] font-medium">
<Link2 className="h-3 w-3 text-blue-500" />
</div>
{Object.entries(joinColumnsByTable).map(([refTable, refColumns]) => (
<div key={refTable} className="mb-1">
<div
className="hover:bg-muted/30 flex cursor-pointer items-center gap-1 rounded px-1 py-0.5"
onClick={() => toggleJoinSection(refTable)}
>
{expandedJoinSections.has(refTable) ? (
<ChevronDown className="text-muted-foreground h-3 w-3 flex-shrink-0" />
) : (
<ChevronRight className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<span className="truncate text-[10px] font-medium text-blue-600">{refTable}</span>
<span className="text-muted-foreground text-[10px]">({refColumns.length})</span>
</div>
{expandedJoinSections.has(refTable) && (
<div className="ml-3 space-y-0.5">
{refColumns.map((column) => (
<div
key={column.columnName}
className={cn(
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
isColumnAdded(column.columnName) && "bg-blue-50",
)}
onClick={() => toggleColumn(column)}
>
<Checkbox
checked={isColumnAdded(column.columnName)}
onCheckedChange={() => toggleColumn(column)}
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
/>
<span className="min-w-0 flex-1 truncate text-xs">{column.displayName}</span>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
{/* 선택된 컬럼 상세 설정 */}
{configColumns.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<Label className="text-xs font-medium"> ({configColumns.length})</Label>
<div className="max-h-40 space-y-1 overflow-y-auto">
{configColumns.map((column, index) => {
const colInfo = columns.find((c) => c.columnName === column.key);
const showThousandSeparator = isNumberColumn(column.key);
return (
<div key={column.key} className="bg-muted/30 space-y-1.5 rounded-md border p-2">
<div className="flex items-center gap-2">
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab" />
{column.isJoinColumn ? (
<Link2 className="h-3 w-3 flex-shrink-0 text-blue-500" />
) : (
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
)}
<Input
value={column.title}
onChange={(e) => updateColumnTitle(column.key, e.target.value)}
placeholder="제목"
className="h-6 flex-1 text-xs"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => toggleColumn(colInfo || { columnName: column.key, displayName: column.title })}
className="text-destructive h-6 w-6 p-0"
>
×
</Button>
</div>
{/* 숫자 컬럼인 경우 천단위 구분자 옵션 표시 */}
{showThousandSeparator && (
<div className="ml-5 flex items-center gap-2">
<Checkbox
id={`thousand-${column.key}`}
checked={column.thousandSeparator !== false}
onCheckedChange={(checked) => toggleThousandSeparator(column.key, !!checked)}
className="h-3 w-3"
/>
<label htmlFor={`thousand-${column.key}`} className="text-muted-foreground text-[10px]">
</label>
</div>
)}
</div>
);
})}
</div>
</div>
</>
)}
<Separator />
{/* 페이지네이션 설정 */}
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="pagination"
checked={config.pagination !== false}
onCheckedChange={(checked) => updateConfig("pagination", checked)}
/>
<label htmlFor="pagination" className="text-xs font-medium">
</label>
</div>
{config.pagination !== false && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={String(config.pageSize || 10)}
onValueChange={(value) => updateConfig("pageSize", Number(value))}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
</div>
<TableListConfigPanel
config={tableListConfig}
onChange={handleConfigChange}
screenTableName={currentTableName}
/>
);
};