Compare commits
3 Commits
e168753d87
...
7181822832
| Author | SHA1 | Date |
|---|---|---|
|
|
7181822832 | |
|
|
57d86c8ef1 | |
|
|
e937ba9161 |
|
|
@ -67,7 +67,90 @@ interface UnifiedRepeaterConfig {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 저장 테이블 설정 UI 표준
|
### 조회 테이블 설정 UI 표준 (테이블 리스트)
|
||||||
|
|
||||||
|
테이블 리스트 등 조회용 컴포넌트의 ConfigPanel에서:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 현재 선택된 테이블 카드 형태로 표시
|
||||||
|
<div className="flex items-center gap-2 rounded-md border bg-slate-50 p-2">
|
||||||
|
<Database className="h-4 w-4 text-blue-500" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium">
|
||||||
|
{config.customTableName || screenTableName || "테이블 미선택"}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-muted-foreground">
|
||||||
|
{config.useCustomTable ? "커스텀 테이블" : "화면 기본 테이블"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// 테이블 선택 Combobox (기본/전체 그룹)
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-full justify-between">
|
||||||
|
테이블 변경...
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." />
|
||||||
|
<CommandList>
|
||||||
|
{/* 그룹 1: 화면 기본 테이블 */}
|
||||||
|
{screenTableName && (
|
||||||
|
<CommandGroup heading="기본 (화면 테이블)">
|
||||||
|
<CommandItem
|
||||||
|
value={screenTableName}
|
||||||
|
onSelect={() => {
|
||||||
|
handleChange("useCustomTable", false);
|
||||||
|
handleChange("customTableName", undefined);
|
||||||
|
handleChange("selectedTable", screenTableName);
|
||||||
|
handleChange("columns", []); // 테이블 변경 시 컬럼 초기화
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Database className="mr-2 h-3 w-3 text-blue-500" />
|
||||||
|
{screenTableName}
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 그룹 2: 전체 테이블 */}
|
||||||
|
<CommandGroup heading="전체 테이블">
|
||||||
|
{availableTables
|
||||||
|
.filter((table) => table.tableName !== screenTableName)
|
||||||
|
.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={table.tableName}
|
||||||
|
onSelect={() => {
|
||||||
|
handleChange("useCustomTable", true);
|
||||||
|
handleChange("customTableName", table.tableName);
|
||||||
|
handleChange("selectedTable", table.tableName);
|
||||||
|
handleChange("columns", []); // 테이블 변경 시 컬럼 초기화
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table2 className="mr-2 h-3 w-3 text-slate-400" />
|
||||||
|
{table.displayName || table.tableName}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
// 읽기전용 설정
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={config.isReadOnly || false}
|
||||||
|
onCheckedChange={(checked) => handleChange("isReadOnly", checked)}
|
||||||
|
/>
|
||||||
|
<Label className="text-xs">읽기전용 (조회만 가능)</Label>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 저장 테이블 설정 UI 표준 (리피터)
|
||||||
|
|
||||||
리피터 등 저장 기능이 있는 컴포넌트의 ConfigPanel에서:
|
리피터 등 저장 기능이 있는 컴포넌트의 ConfigPanel에서:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4076,12 +4076,17 @@ export class TableManagementService {
|
||||||
|
|
||||||
// table_type_columns에서 입력타입 정보 조회
|
// table_type_columns에서 입력타입 정보 조회
|
||||||
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
// 회사별 설정 우선, 없으면 기본 설정(*) fallback
|
||||||
|
// detail_settings 컬럼에 유효하지 않은 JSON이 있을 수 있으므로 안전하게 처리
|
||||||
const rawInputTypes = await query<any>(
|
const rawInputTypes = await query<any>(
|
||||||
`SELECT DISTINCT ON (ttc.column_name)
|
`SELECT DISTINCT ON (ttc.column_name)
|
||||||
ttc.column_name as "columnName",
|
ttc.column_name as "columnName",
|
||||||
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
COALESCE(cl.column_label, ttc.column_name) as "displayName",
|
||||||
ttc.input_type as "inputType",
|
ttc.input_type as "inputType",
|
||||||
COALESCE(ttc.detail_settings::jsonb, '{}'::jsonb) as "detailSettings",
|
CASE
|
||||||
|
WHEN ttc.detail_settings IS NULL OR ttc.detail_settings = '' THEN '{}'::jsonb
|
||||||
|
WHEN ttc.detail_settings ~ '^\\s*\\{.*\\}\\s*$' THEN ttc.detail_settings::jsonb
|
||||||
|
ELSE '{}'::jsonb
|
||||||
|
END as "detailSettings",
|
||||||
ttc.is_nullable as "isNullable",
|
ttc.is_nullable as "isNullable",
|
||||||
ic.data_type as "dataType",
|
ic.data_type as "dataType",
|
||||||
ttc.company_code as "companyCode"
|
ttc.company_code as "companyCode"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
# 컴포넌트 기능 현황
|
||||||
|
|
||||||
|
> 작성일: 2026-01-15
|
||||||
|
> 현재 사용 가능한 16개 컴포넌트의 다국어 지원 및 테이블 설정 기능 현황
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 요약
|
||||||
|
|
||||||
|
| 기능 | 적용 완료 | 미적용 | 해당없음 |
|
||||||
|
| -------------------------- | --------- | ------ | -------- |
|
||||||
|
| **다국어 지원** | 3개 | 10개 | 3개 |
|
||||||
|
| **컴포넌트별 테이블 설정** | 5개 | 5개 | 6개 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 컴포넌트별 상세 현황
|
||||||
|
|
||||||
|
### 데이터 표시 (Display) - 4개
|
||||||
|
|
||||||
|
| 컴포넌트 | 다국어 지원 | 테이블 설정 | 비고 |
|
||||||
|
| ------------------- | :---------: | :---------: | ------------------------------------------------------------------- |
|
||||||
|
| **테이블 리스트** | ✅ 적용 | ✅ 적용 | `customTableName`, `useCustomTable` 지원 |
|
||||||
|
| **카드 디스플레이** | ❌ 미적용 | ✅ 적용 | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable` 지원 |
|
||||||
|
| **텍스트 표시** | ❌ 미적용 | ➖ 해당없음 | 정적 텍스트 표시용 |
|
||||||
|
| **피벗 그리드** | ❌ 미적용 | ⚠️ 부분 | `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`)
|
||||||
|
|
||||||
|
컴포넌트별 테이블 설정이란 화면의 메인 테이블과 별개로 컴포넌트가 자체적으로 사용할 테이블을 지정할 수 있는 기능입니다.
|
||||||
|
|
||||||
|
**완전 적용 (5개)**
|
||||||
|
|
||||||
|
| 컴포넌트 | 적용 방식 |
|
||||||
|
| ------------------ | --------------------------------------------------------------------------------- |
|
||||||
|
| `table-list` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable`, `isReadOnly` 지원 |
|
||||||
|
| `unified-repeater` | Combobox UI로 테이블 선택, `mainTableName`, `foreignKeyColumn` 지원, FK 자동 연결 |
|
||||||
|
| `unified-list` | `TableListConfigPanel` 래핑하여 동일 기능 제공 |
|
||||||
|
| `card-display` | Combobox UI로 테이블 선택, `customTableName`, `useCustomTable` 지원 |
|
||||||
|
|
||||||
|
**부분 적용 (5개)**
|
||||||
|
|
||||||
|
| 컴포넌트 | 현재 상태 | 필요 작업 |
|
||||||
|
| ------------------------ | --------------------------- | --------------------- |
|
||||||
|
| `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 미비) |
|
||||||
|
| ❌ | 미적용 |
|
||||||
|
| ➖ | 해당없음 (기능 불필요) |
|
||||||
|
|
@ -66,14 +66,7 @@ export function ComponentsPanel({
|
||||||
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
// unified-date: 테이블 컬럼 드래그 시 자동 생성되므로 숨김 처리
|
||||||
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
// unified-layout: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
||||||
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
// unified-group: 중첩 드래그앤드롭 기능 미구현으로 숨김 처리
|
||||||
{
|
// unified-list: table-list, card-display로 분리하여 숨김 처리
|
||||||
id: "unified-list",
|
|
||||||
name: "통합 목록",
|
|
||||||
description: "테이블, 카드 등 다양한 데이터 표시 방식 지원",
|
|
||||||
category: "display" as ComponentCategory,
|
|
||||||
tags: ["table", "list", "card", "unified"],
|
|
||||||
defaultSize: { width: 600, height: 400 },
|
|
||||||
},
|
|
||||||
// unified-media 제거 - 테이블 컬럼의 image/file 입력 타입으로 사용
|
// unified-media 제거 - 테이블 컬럼의 image/file 입력 타입으로 사용
|
||||||
// unified-biz 제거 - 개별 컴포넌트(flow-widget, rack-structure, numbering-rule)로 직접 표시
|
// unified-biz 제거 - 개별 컴포넌트(flow-widget, rack-structure, numbering-rule)로 직접 표시
|
||||||
// unified-hierarchy 제거 - 현재 미사용
|
// unified-hierarchy 제거 - 현재 미사용
|
||||||
|
|
@ -113,8 +106,7 @@ export function ComponentsPanel({
|
||||||
// 특수 업무용 컴포넌트 (일반 화면에서 불필요)
|
// 특수 업무용 컴포넌트 (일반 화면에서 불필요)
|
||||||
"tax-invoice-list", // 세금계산서 전용
|
"tax-invoice-list", // 세금계산서 전용
|
||||||
"customer-item-mapping", // 고객-품목 매핑 전용
|
"customer-item-mapping", // 고객-품목 매핑 전용
|
||||||
// unified-list로 통합됨
|
// card-display는 별도 컴포넌트로 유지
|
||||||
"card-display", // → unified-list (card 모드)
|
|
||||||
// unified-media로 통합됨
|
// unified-media로 통합됨
|
||||||
"image-display", // → unified-media (image)
|
"image-display", // → unified-media (image)
|
||||||
// 공통코드관리로 통합 예정
|
// 공통코드관리로 통합 예정
|
||||||
|
|
@ -128,6 +120,12 @@ export function ComponentsPanel({
|
||||||
"universal-form-modal", // 범용 폼 모달
|
"universal-form-modal", // 범용 폼 모달
|
||||||
// 통합 미디어 (테이블 컬럼 입력 타입으로 사용)
|
// 통합 미디어 (테이블 컬럼 입력 타입으로 사용)
|
||||||
"unified-media", // → 테이블 컬럼의 image/file 입력 타입으로 사용
|
"unified-media", // → 테이블 컬럼의 image/file 입력 타입으로 사용
|
||||||
|
// 플로우 위젯 숨김 처리
|
||||||
|
"flow-widget",
|
||||||
|
// 선택 항목 상세입력 - 기존 컴포넌트 조합으로 대체 가능
|
||||||
|
"selected-items-detail-input",
|
||||||
|
// 연관 데이터 버튼 - unified-repeater로 대체 가능
|
||||||
|
"related-data-buttons",
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,13 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UnifiedList 설정 패널
|
* UnifiedList 설정 패널
|
||||||
* 통합 목록 컴포넌트의 세부 설정을 관리합니다.
|
* TableListConfigPanel을 래핑하여 동일한 설정 기능을 제공합니다.
|
||||||
* - 현재 화면의 테이블 데이터를 사용
|
* 카드 표시는 별도의 card-display 컴포넌트를 사용합니다.
|
||||||
* - 테이블 컬럼 + 엔티티 조인 컬럼 선택 지원
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { TableListConfigPanel } from "@/lib/registry/components/table-list/TableListConfigPanel";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { TableListConfig } from "@/lib/registry/components/table-list/types";
|
||||||
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";
|
|
||||||
|
|
||||||
interface UnifiedListConfigPanelProps {
|
interface UnifiedListConfigPanelProps {
|
||||||
config: Record<string, any>;
|
config: Record<string, any>;
|
||||||
|
|
@ -25,476 +17,148 @@ interface UnifiedListConfigPanelProps {
|
||||||
currentTableName?: string;
|
currentTableName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColumnOption {
|
/**
|
||||||
columnName: string;
|
* UnifiedList 설정 패널
|
||||||
displayName: string;
|
* TableListConfigPanel과 동일한 기능을 제공
|
||||||
isJoinColumn?: boolean;
|
*/
|
||||||
sourceTable?: string;
|
|
||||||
inputType?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
currentTableName,
|
currentTableName,
|
||||||
}) => {
|
}) => {
|
||||||
// 컬럼 목록 (테이블 컬럼 + 엔티티 조인 컬럼)
|
// UnifiedList config를 TableListConfig 형식으로 변환
|
||||||
const [columns, setColumns] = useState<ColumnOption[]>([]);
|
const tableListConfig: TableListConfig = useMemo(() => {
|
||||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
// 컬럼 형식 변환: UnifiedList columns -> TableList columns
|
||||||
const [expandedJoinSections, setExpandedJoinSections] = useState<Set<string>>(new Set());
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
// 설정 업데이트 핸들러
|
return {
|
||||||
const updateConfig = (field: string, value: any) => {
|
selectedTable: config.tableName || config.dataSource?.table || currentTableName,
|
||||||
const newConfig = { ...config, [field]: value };
|
tableName: config.tableName || config.dataSource?.table || currentTableName,
|
||||||
console.log("⚙️ UnifiedListConfigPanel updateConfig:", { field, value, newConfig });
|
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);
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<TableListConfigPanel
|
||||||
{/* 뷰 모드 */}
|
config={tableListConfig}
|
||||||
<div className="space-y-2">
|
onChange={handleConfigChange}
|
||||||
<Label className="text-xs font-medium">표시 방식</Label>
|
screenTableName={currentTableName}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||||||
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
|
|
@ -11,11 +12,14 @@ import {
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2, Database, ChevronsUpDown, Check } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface CardDisplayConfigPanelProps {
|
interface CardDisplayConfigPanelProps {
|
||||||
config: any;
|
config: any;
|
||||||
|
|
@ -57,6 +61,13 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
screenTableName,
|
screenTableName,
|
||||||
tableColumns = [],
|
tableColumns = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
// 테이블 선택 상태
|
||||||
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
|
||||||
|
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
||||||
|
const [loadingTables, setLoadingTables] = useState(false);
|
||||||
|
const [availableColumns, setAvailableColumns] = useState<any[]>([]);
|
||||||
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||||
|
|
||||||
// 엔티티 조인 컬럼 상태
|
// 엔티티 조인 컬럼 상태
|
||||||
const [entityJoinColumns, setEntityJoinColumns] = useState<{
|
const [entityJoinColumns, setEntityJoinColumns] = useState<{
|
||||||
availableColumns: EntityJoinColumn[];
|
availableColumns: EntityJoinColumn[];
|
||||||
|
|
@ -64,18 +75,80 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
}>({ availableColumns: [], joinTables: [] });
|
}>({ availableColumns: [], joinTables: [] });
|
||||||
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
|
||||||
|
|
||||||
|
// 현재 사용할 테이블명
|
||||||
|
const targetTableName = useMemo(() => {
|
||||||
|
if (config.useCustomTable && config.customTableName) {
|
||||||
|
return config.customTableName;
|
||||||
|
}
|
||||||
|
return config.tableName || screenTableName;
|
||||||
|
}, [config.useCustomTable, config.customTableName, config.tableName, screenTableName]);
|
||||||
|
|
||||||
|
// 전체 테이블 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAllTables = async () => {
|
||||||
|
setLoadingTables(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getTableList();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setAllTables(response.data.map((t: any) => ({
|
||||||
|
tableName: t.tableName || t.table_name,
|
||||||
|
displayName: t.tableLabel || t.displayName || t.tableName || t.table_name,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("테이블 목록 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setLoadingTables(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadAllTables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 선택된 테이블의 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadColumns = async () => {
|
||||||
|
if (!targetTableName) {
|
||||||
|
setAvailableColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 커스텀 테이블이 아니면 props로 받은 tableColumns 사용
|
||||||
|
if (!config.useCustomTable && tableColumns && tableColumns.length > 0) {
|
||||||
|
setAvailableColumns(tableColumns);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingColumns(true);
|
||||||
|
try {
|
||||||
|
const result = await tableManagementApi.getColumnList(targetTableName);
|
||||||
|
if (result.success && result.data?.columns) {
|
||||||
|
setAvailableColumns(result.data.columns.map((col: any) => ({
|
||||||
|
columnName: col.columnName,
|
||||||
|
columnLabel: col.displayName || col.columnLabel || col.columnName,
|
||||||
|
dataType: col.dataType,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("컬럼 목록 로드 실패:", error);
|
||||||
|
setAvailableColumns([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingColumns(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadColumns();
|
||||||
|
}, [targetTableName, config.useCustomTable, tableColumns]);
|
||||||
|
|
||||||
// 엔티티 조인 컬럼 정보 가져오기
|
// 엔티티 조인 컬럼 정보 가져오기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchEntityJoinColumns = async () => {
|
const fetchEntityJoinColumns = async () => {
|
||||||
const tableName = config.tableName || screenTableName;
|
if (!targetTableName) {
|
||||||
if (!tableName) {
|
|
||||||
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingEntityJoins(true);
|
setLoadingEntityJoins(true);
|
||||||
try {
|
try {
|
||||||
const result = await entityJoinApi.getEntityJoinColumns(tableName);
|
const result = await entityJoinApi.getEntityJoinColumns(targetTableName);
|
||||||
setEntityJoinColumns({
|
setEntityJoinColumns({
|
||||||
availableColumns: result.availableColumns || [],
|
availableColumns: result.availableColumns || [],
|
||||||
joinTables: result.joinTables || [],
|
joinTables: result.joinTables || [],
|
||||||
|
|
@ -89,7 +162,38 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchEntityJoinColumns();
|
fetchEntityJoinColumns();
|
||||||
}, [config.tableName, screenTableName]);
|
}, [targetTableName]);
|
||||||
|
|
||||||
|
// 테이블 선택 핸들러
|
||||||
|
const handleTableSelect = (tableName: string, isScreenTable: boolean) => {
|
||||||
|
if (isScreenTable) {
|
||||||
|
// 화면 기본 테이블 선택
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
useCustomTable: false,
|
||||||
|
customTableName: undefined,
|
||||||
|
tableName: tableName,
|
||||||
|
columnMapping: { displayColumns: [] }, // 컬럼 매핑 초기화
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 다른 테이블 선택
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
useCustomTable: true,
|
||||||
|
customTableName: tableName,
|
||||||
|
tableName: tableName,
|
||||||
|
columnMapping: { displayColumns: [] }, // 컬럼 매핑 초기화
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTableComboboxOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 현재 선택된 테이블 표시명 가져오기
|
||||||
|
const getSelectedTableDisplay = () => {
|
||||||
|
if (!targetTableName) return "테이블을 선택하세요";
|
||||||
|
const found = allTables.find(t => t.tableName === targetTableName);
|
||||||
|
return found?.displayName || targetTableName;
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (key: string, value: any) => {
|
const handleChange = (key: string, value: any) => {
|
||||||
onChange({ ...config, [key]: value });
|
onChange({ ...config, [key]: value });
|
||||||
|
|
@ -219,6 +323,9 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
joinColumnsByTable[col.tableName].push(col);
|
joinColumnsByTable[col.tableName].push(col);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 현재 사용할 컬럼 목록 (커스텀 테이블이면 로드한 컬럼, 아니면 props)
|
||||||
|
const currentTableColumns = config.useCustomTable ? availableColumns : (tableColumns.length > 0 ? tableColumns : availableColumns);
|
||||||
|
|
||||||
// 컬럼 선택 셀렉트 박스 렌더링 (Shadcn UI)
|
// 컬럼 선택 셀렉트 박스 렌더링 (Shadcn UI)
|
||||||
const renderColumnSelect = (
|
const renderColumnSelect = (
|
||||||
value: string,
|
value: string,
|
||||||
|
|
@ -240,12 +347,12 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
{/* 기본 테이블 컬럼 */}
|
{/* 기본 테이블 컬럼 */}
|
||||||
{tableColumns.length > 0 && (
|
{currentTableColumns.length > 0 && (
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel className="text-xs font-semibold text-muted-foreground">
|
<SelectLabel className="text-xs font-semibold text-muted-foreground">
|
||||||
기본 컬럼
|
기본 컬럼
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
{tableColumns.map((column) => (
|
{currentTableColumns.map((column: any) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
value={column.columnName}
|
value={column.columnName}
|
||||||
|
|
@ -283,13 +390,99 @@ export const CardDisplayConfigPanel: React.FC<CardDisplayConfigPanelProps> = ({
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-sm font-medium">카드 디스플레이 설정</div>
|
<div className="text-sm font-medium">카드 디스플레이 설정</div>
|
||||||
|
|
||||||
|
{/* 테이블 선택 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs font-medium">표시 테이블</Label>
|
||||||
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={tableComboboxOpen}
|
||||||
|
className="h-9 w-full justify-between text-xs"
|
||||||
|
disabled={loadingTables}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 truncate">
|
||||||
|
<Database className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="truncate">{getSelectedTableDisplay()}</span>
|
||||||
|
</div>
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[300px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="py-3 text-center text-xs text-muted-foreground">
|
||||||
|
테이블을 찾을 수 없습니다.
|
||||||
|
</CommandEmpty>
|
||||||
|
|
||||||
|
{/* 화면 기본 테이블 */}
|
||||||
|
{screenTableName && (
|
||||||
|
<CommandGroup heading="기본 (화면 테이블)">
|
||||||
|
<CommandItem
|
||||||
|
value={screenTableName}
|
||||||
|
onSelect={() => handleTableSelect(screenTableName, true)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
targetTableName === screenTableName && !config.useCustomTable
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Database className="mr-2 h-3.5 w-3.5 text-blue-500" />
|
||||||
|
{allTables.find(t => t.tableName === screenTableName)?.displayName || screenTableName}
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 전체 테이블 */}
|
||||||
|
<CommandGroup heading="전체 테이블">
|
||||||
|
{allTables
|
||||||
|
.filter(t => t.tableName !== screenTableName)
|
||||||
|
.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={table.tableName}
|
||||||
|
onSelect={() => handleTableSelect(table.tableName, false)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
config.useCustomTable && targetTableName === table.tableName
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Database className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="truncate">{table.displayName}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{config.useCustomTable && (
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
화면 기본 테이블이 아닌 다른 테이블의 데이터를 표시합니다.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
|
{/* 테이블이 선택된 경우 컬럼 매핑 설정 */}
|
||||||
{tableColumns && tableColumns.length > 0 && (
|
{(currentTableColumns.length > 0 || loadingColumns) && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h5 className="text-xs font-medium text-muted-foreground">컬럼 매핑</h5>
|
<h5 className="text-xs font-medium text-muted-foreground">컬럼 매핑</h5>
|
||||||
|
|
||||||
{loadingEntityJoins && (
|
{(loadingEntityJoins || loadingColumns) && (
|
||||||
<div className="text-xs text-muted-foreground">조인 컬럼 로딩 중...</div>
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{loadingColumns ? "컬럼 로딩 중..." : "조인 컬럼 로딩 중..."}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,12 @@ export interface CardDisplayConfig extends ComponentConfig {
|
||||||
// 컬럼 매핑 설정
|
// 컬럼 매핑 설정
|
||||||
columnMapping?: ColumnMappingConfig;
|
columnMapping?: ColumnMappingConfig;
|
||||||
|
|
||||||
|
// 컴포넌트별 테이블 설정
|
||||||
|
useCustomTable?: boolean;
|
||||||
|
customTableName?: string;
|
||||||
|
tableName?: string;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
|
||||||
// 테이블 데이터 설정
|
// 테이블 데이터 설정
|
||||||
dataSource?: "static" | "table" | "api";
|
dataSource?: "static" | "table" | "api";
|
||||||
tableId?: string;
|
tableId?: string;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue