좌측 선택데이터에 대한 우측에 데이터표시및 버튼표시 컴포넌트
This commit is contained in:
parent
3a55ea3b64
commit
a73b37f558
|
|
@ -26,7 +26,14 @@ export const dataApi = {
|
|||
size: number;
|
||||
totalPages: number;
|
||||
}> => {
|
||||
const response = await apiClient.get(`/data/${tableName}`, { params });
|
||||
// filters를 평탄화하여 쿼리 파라미터로 전달 (백엔드 ...filters 형식에 맞춤)
|
||||
const { filters, ...restParams } = params || {};
|
||||
const flattenedParams = {
|
||||
...restParams,
|
||||
...(filters || {}), // filters 객체를 평탄화
|
||||
};
|
||||
|
||||
const response = await apiClient.get(`/data/${tableName}`, { params: flattenedParams });
|
||||
const raw = response.data || {};
|
||||
const items: any[] = (raw.data ?? raw.items ?? raw.rows ?? []) as any[];
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ import "./tax-invoice-list/TaxInvoiceListRenderer"; // 세금계산서 목록,
|
|||
// 🆕 메일 수신자 선택 컴포넌트
|
||||
import "./mail-recipient-selector/MailRecipientSelectorRenderer"; // 내부 인원 선택 + 외부 이메일 입력
|
||||
|
||||
// 🆕 연관 데이터 버튼 컴포넌트
|
||||
import "./related-data-buttons/RelatedDataButtonsRenderer"; // 좌측 선택 데이터 기반 연관 테이블 버튼 표시
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
# RelatedDataButtons 컴포넌트
|
||||
|
||||
좌측 패널에서 선택한 데이터를 기반으로 연관 테이블의 데이터를 버튼으로 표시하는 컴포넌트
|
||||
|
||||
## 개요
|
||||
|
||||
- **ID**: `related-data-buttons`
|
||||
- **카테고리**: data
|
||||
- **웹타입**: container
|
||||
- **버전**: 1.0.0
|
||||
|
||||
## 사용 사례
|
||||
|
||||
### 품목별 라우팅 버전 관리
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 알루미늄 프레임 [+ 라우팅 버전 추가] │
|
||||
│ ITEM001 │
|
||||
│ ┌──────────────┐ ┌─────────┐ │
|
||||
│ │ 기본 라우팅 ★ │ │ 개선버전 │ │
|
||||
│ └──────────────┘ └─────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 데이터 흐름
|
||||
|
||||
```
|
||||
1. 좌측 패널: item_info 선택
|
||||
↓ SplitPanelContext.selectedLeftData
|
||||
2. RelatedDataButtons: item_code로 item_routing_version 조회
|
||||
↓ 버튼 클릭 시 이벤트 발생
|
||||
3. 하위 테이블: routing_version_id로 item_routing_detail 필터링
|
||||
```
|
||||
|
||||
## 설정 옵션
|
||||
|
||||
### 소스 매핑 (sourceMapping)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| sourceTable | string | 좌측 패널 테이블명 (예: item_info) |
|
||||
| sourceColumn | string | 필터에 사용할 컬럼 (예: item_code) |
|
||||
|
||||
### 헤더 표시 (headerDisplay)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| show | boolean | 헤더 표시 여부 |
|
||||
| titleColumn | string | 제목으로 표시할 컬럼 (예: item_name) |
|
||||
| subtitleColumn | string | 부제목으로 표시할 컬럼 (예: item_code) |
|
||||
|
||||
### 버튼 데이터 소스 (buttonDataSource)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| tableName | string | 조회할 테이블명 (예: item_routing_version) |
|
||||
| filterColumn | string | 필터링할 컬럼명 (예: item_code) |
|
||||
| displayColumn | string | 버튼에 표시할 컬럼명 (예: version_name) |
|
||||
| valueColumn | string | 선택 시 전달할 값 컬럼 (기본: id) |
|
||||
| orderColumn | string | 정렬 컬럼 |
|
||||
| orderDirection | "ASC" \| "DESC" | 정렬 방향 |
|
||||
|
||||
### 버튼 스타일 (buttonStyle)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| variant | string | 기본 버튼 스타일 (default, outline, secondary, ghost) |
|
||||
| activeVariant | string | 선택 시 버튼 스타일 |
|
||||
| size | string | 버튼 크기 (sm, default, lg) |
|
||||
| defaultIndicator.column | string | 기본 버전 판단 컬럼 |
|
||||
| defaultIndicator.showStar | boolean | 별표 아이콘 표시 여부 |
|
||||
|
||||
### 추가 버튼 (addButton)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| show | boolean | 추가 버튼 표시 여부 |
|
||||
| label | string | 버튼 라벨 |
|
||||
| position | "header" \| "inline" | 버튼 위치 |
|
||||
| modalScreenId | number | 연결할 모달 화면 ID |
|
||||
|
||||
### 이벤트 설정 (events)
|
||||
|
||||
| 속성 | 타입 | 설명 |
|
||||
|------|------|------|
|
||||
| targetTable | string | 필터링할 하위 테이블명 |
|
||||
| targetFilterColumn | string | 하위 테이블의 필터 컬럼명 |
|
||||
|
||||
## 이벤트
|
||||
|
||||
### related-button-select
|
||||
|
||||
버튼 선택 시 발생하는 커스텀 이벤트
|
||||
|
||||
```typescript
|
||||
window.addEventListener("related-button-select", (e: CustomEvent) => {
|
||||
const { targetTable, filterColumn, filterValue, selectedData } = e.detail;
|
||||
// 하위 테이블 필터링 처리
|
||||
});
|
||||
```
|
||||
|
||||
## 사용 예시
|
||||
|
||||
### 품목별 라우팅 버전 화면
|
||||
|
||||
```typescript
|
||||
const config: RelatedDataButtonsConfig = {
|
||||
sourceMapping: {
|
||||
sourceTable: "item_info",
|
||||
sourceColumn: "item_code",
|
||||
},
|
||||
headerDisplay: {
|
||||
show: true,
|
||||
titleColumn: "item_name",
|
||||
subtitleColumn: "item_code",
|
||||
},
|
||||
buttonDataSource: {
|
||||
tableName: "item_routing_version",
|
||||
filterColumn: "item_code",
|
||||
displayColumn: "version_name",
|
||||
valueColumn: "id",
|
||||
},
|
||||
buttonStyle: {
|
||||
variant: "outline",
|
||||
activeVariant: "default",
|
||||
defaultIndicator: {
|
||||
column: "is_default",
|
||||
showStar: true,
|
||||
},
|
||||
},
|
||||
events: {
|
||||
targetTable: "item_routing_detail",
|
||||
targetFilterColumn: "routing_version_id",
|
||||
},
|
||||
addButton: {
|
||||
show: true,
|
||||
label: "+ 라우팅 버전 추가",
|
||||
position: "header",
|
||||
},
|
||||
autoSelectFirst: true,
|
||||
};
|
||||
```
|
||||
|
||||
## 분할 패널과 함께 사용
|
||||
|
||||
```
|
||||
┌─────────────────┬──────────────────────────────────────────────┐
|
||||
│ │ [RelatedDataButtons 컴포넌트] │
|
||||
│ 품목 목록 │ 품목명 표시 + 버전 버튼들 │
|
||||
│ (좌측 패널) ├──────────────────────────────────────────────┤
|
||||
│ │ [DataTable 컴포넌트] │
|
||||
│ item_info │ 공정 순서 테이블 (item_routing_detail) │
|
||||
│ │ related-button-select 이벤트로 필터링 │
|
||||
└─────────────────┴──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 개발자 정보
|
||||
|
||||
- **생성일**: 2024-12
|
||||
- **경로**: `lib/registry/components/related-data-buttons/`
|
||||
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Star, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import type { RelatedDataButtonsConfig, ButtonItem } from "./types";
|
||||
|
||||
interface RelatedDataButtonsComponentProps {
|
||||
config: RelatedDataButtonsConfig;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const RelatedDataButtonsComponent: React.FC<RelatedDataButtonsComponentProps> = ({
|
||||
config,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
const [buttons, setButtons] = useState<ButtonItem[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [masterData, setMasterData] = useState<Record<string, any> | null>(null);
|
||||
|
||||
// SplitPanel Context 연결
|
||||
const splitPanelContext = useSplitPanelContext();
|
||||
|
||||
// 좌측 패널에서 선택된 데이터 감지
|
||||
useEffect(() => {
|
||||
if (!splitPanelContext?.selectedLeftData) {
|
||||
setMasterData(null);
|
||||
setButtons([]);
|
||||
setSelectedId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setMasterData(splitPanelContext.selectedLeftData);
|
||||
}, [splitPanelContext?.selectedLeftData]);
|
||||
|
||||
// 버튼 데이터 로드
|
||||
const loadButtons = useCallback(async () => {
|
||||
if (!masterData || !config.buttonDataSource?.tableName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterValue = masterData[config.sourceMapping.sourceColumn];
|
||||
if (!filterValue) {
|
||||
setButtons([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { tableName, filterColumn, displayColumn, valueColumn, orderColumn, orderDirection } = config.buttonDataSource;
|
||||
|
||||
const response = await dataApi.getTableData(tableName, {
|
||||
filters: { [filterColumn]: filterValue },
|
||||
sortBy: orderColumn || "created_date",
|
||||
sortOrder: (orderDirection?.toLowerCase() || "asc") as "asc" | "desc",
|
||||
size: 50,
|
||||
});
|
||||
|
||||
if (response.data && response.data.length > 0) {
|
||||
const defaultConfig = config.buttonStyle?.defaultIndicator;
|
||||
|
||||
const items: ButtonItem[] = response.data.map((row: Record<string, any>) => {
|
||||
let isDefault = false;
|
||||
if (defaultConfig?.column) {
|
||||
const val = row[defaultConfig.column];
|
||||
const checkValue = defaultConfig.value || "Y";
|
||||
isDefault = val === checkValue || val === true || val === "true";
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id || row[valueColumn || "id"],
|
||||
displayText: row[displayColumn] || row.id,
|
||||
value: row[valueColumn || "id"],
|
||||
isDefault,
|
||||
rawData: row,
|
||||
};
|
||||
});
|
||||
|
||||
setButtons(items);
|
||||
|
||||
// 자동 선택: 기본 항목 또는 첫 번째 항목
|
||||
if (config.autoSelectFirst && items.length > 0) {
|
||||
const defaultItem = items.find(item => item.isDefault);
|
||||
const targetItem = defaultItem || items[0];
|
||||
setSelectedId(targetItem.id);
|
||||
emitSelection(targetItem);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("RelatedDataButtons 데이터 로드 실패:", error);
|
||||
setButtons([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [masterData, config.buttonDataSource, config.sourceMapping, config.buttonStyle, config.autoSelectFirst]);
|
||||
|
||||
// masterData 변경 시 버튼 로드
|
||||
useEffect(() => {
|
||||
if (masterData) {
|
||||
setSelectedId(null); // 마스터 변경 시 선택 초기화
|
||||
loadButtons();
|
||||
}
|
||||
}, [masterData, loadButtons]);
|
||||
|
||||
// 선택 이벤트 발생
|
||||
const emitSelection = useCallback((item: ButtonItem) => {
|
||||
if (!config.events?.targetTable || !config.events?.targetFilterColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 커스텀 이벤트 발생 (하위 테이블 필터링용)
|
||||
window.dispatchEvent(new CustomEvent("related-button-select", {
|
||||
detail: {
|
||||
targetTable: config.events.targetTable,
|
||||
filterColumn: config.events.targetFilterColumn,
|
||||
filterValue: item.value,
|
||||
selectedData: item.rawData,
|
||||
},
|
||||
}));
|
||||
|
||||
console.log("📌 RelatedDataButtons 선택 이벤트:", {
|
||||
targetTable: config.events.targetTable,
|
||||
filterColumn: config.events.targetFilterColumn,
|
||||
filterValue: item.value,
|
||||
});
|
||||
}, [config.events]);
|
||||
|
||||
// 버튼 클릭 핸들러
|
||||
const handleButtonClick = useCallback((item: ButtonItem) => {
|
||||
setSelectedId(item.id);
|
||||
emitSelection(item);
|
||||
}, [emitSelection]);
|
||||
|
||||
// 추가 버튼 클릭
|
||||
const handleAddClick = useCallback(() => {
|
||||
if (!config.addButton?.modalScreenId) return;
|
||||
|
||||
const filterValue = masterData?.[config.sourceMapping.sourceColumn];
|
||||
|
||||
window.dispatchEvent(new CustomEvent("open-screen-modal", {
|
||||
detail: {
|
||||
screenId: config.addButton.modalScreenId,
|
||||
initialData: {
|
||||
[config.buttonDataSource.filterColumn]: filterValue,
|
||||
},
|
||||
onSuccess: () => {
|
||||
loadButtons(); // 모달 성공 후 새로고침
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, [config.addButton, config.buttonDataSource.filterColumn, config.sourceMapping.sourceColumn, masterData, loadButtons]);
|
||||
|
||||
// 버튼 variant 계산
|
||||
const getButtonVariant = useCallback((item: ButtonItem): "default" | "outline" | "secondary" | "ghost" => {
|
||||
if (selectedId === item.id) {
|
||||
return config.buttonStyle?.activeVariant || "default";
|
||||
}
|
||||
return config.buttonStyle?.variant || "outline";
|
||||
}, [selectedId, config.buttonStyle]);
|
||||
|
||||
// 마스터 데이터 없음
|
||||
if (!masterData) {
|
||||
return (
|
||||
<div className={cn("rounded-lg border bg-card p-4", className)} style={style}>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
좌측에서 항목을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const headerConfig = config.headerDisplay;
|
||||
const addButtonConfig = config.addButton;
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-lg border bg-card", className)} style={style}>
|
||||
{/* 헤더 영역 */}
|
||||
{headerConfig?.show !== false && (
|
||||
<div className="flex items-start justify-between p-4 pb-3">
|
||||
<div>
|
||||
{/* 제목 (품목명 등) */}
|
||||
{headerConfig?.titleColumn && masterData[headerConfig.titleColumn] && (
|
||||
<h3 className="text-lg font-semibold">
|
||||
{masterData[headerConfig.titleColumn]}
|
||||
</h3>
|
||||
)}
|
||||
{/* 부제목 (품목코드 등) */}
|
||||
{headerConfig?.subtitleColumn && masterData[headerConfig.subtitleColumn] && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{masterData[headerConfig.subtitleColumn]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 헤더 위치 추가 버튼 */}
|
||||
{addButtonConfig?.show && addButtonConfig?.position === "header" && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleAddClick}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{addButtonConfig.label || "버전 추가"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="px-4 pb-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : buttons.length === 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{config.emptyMessage || "데이터가 없습니다"}
|
||||
</p>
|
||||
{/* 인라인 추가 버튼 (데이터 없을 때) */}
|
||||
{addButtonConfig?.show && addButtonConfig?.position !== "header" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddClick}
|
||||
className="border-dashed"
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{addButtonConfig.label || "추가"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{buttons.map((item) => (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={getButtonVariant(item)}
|
||||
size={config.buttonStyle?.size || "default"}
|
||||
onClick={() => handleButtonClick(item)}
|
||||
className={cn(
|
||||
"relative",
|
||||
selectedId === item.id && "ring-2 ring-primary ring-offset-1"
|
||||
)}
|
||||
>
|
||||
{/* 기본 버전 별표 */}
|
||||
{item.isDefault && config.buttonStyle?.defaultIndicator?.showStar && (
|
||||
<Star className="mr-1.5 h-3.5 w-3.5 fill-yellow-400 text-yellow-400" />
|
||||
)}
|
||||
{item.displayText}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* 인라인 추가 버튼 */}
|
||||
{addButtonConfig?.show && addButtonConfig?.position !== "header" && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size={config.buttonStyle?.size || "default"}
|
||||
onClick={handleAddClick}
|
||||
className="border-dashed"
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{addButtonConfig.label || "추가"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedDataButtonsComponent;
|
||||
|
|
@ -0,0 +1,558 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement";
|
||||
import type { RelatedDataButtonsConfig } from "./types";
|
||||
|
||||
interface TableInfo {
|
||||
tableName: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
interface ColumnInfo {
|
||||
columnName: string;
|
||||
columnLabel?: string;
|
||||
}
|
||||
|
||||
interface RelatedDataButtonsConfigPanelProps {
|
||||
config: RelatedDataButtonsConfig;
|
||||
onChange: (config: RelatedDataButtonsConfig) => void;
|
||||
tables?: TableInfo[];
|
||||
}
|
||||
|
||||
export const RelatedDataButtonsConfigPanel: React.FC<RelatedDataButtonsConfigPanelProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
tables: propTables = [],
|
||||
}) => {
|
||||
const [allTables, setAllTables] = useState<TableInfo[]>([]);
|
||||
const [sourceTableColumns, setSourceTableColumns] = useState<ColumnInfo[]>([]);
|
||||
const [buttonTableColumns, setButtonTableColumns] = useState<ColumnInfo[]>([]);
|
||||
|
||||
// Popover 상태
|
||||
const [sourceTableOpen, setSourceTableOpen] = useState(false);
|
||||
const [buttonTableOpen, setButtonTableOpen] = useState(false);
|
||||
|
||||
// 전체 테이블 로드
|
||||
useEffect(() => {
|
||||
const loadTables = async () => {
|
||||
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.table_label || t.displayName,
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테이블 목록 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadTables();
|
||||
}, []);
|
||||
|
||||
// 소스 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
if (!config.sourceMapping?.sourceTable) {
|
||||
setSourceTableColumns([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await getTableColumns(config.sourceMapping.sourceTable);
|
||||
if (response.success && response.data?.columns) {
|
||||
setSourceTableColumns(response.data.columns.map((c: any) => ({
|
||||
columnName: c.columnName || c.column_name,
|
||||
columnLabel: c.columnLabel || c.column_label || c.displayName,
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadColumns();
|
||||
}, [config.sourceMapping?.sourceTable]);
|
||||
|
||||
// 버튼 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
if (!config.buttonDataSource?.tableName) {
|
||||
setButtonTableColumns([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await getTableColumns(config.buttonDataSource.tableName);
|
||||
if (response.success && response.data?.columns) {
|
||||
setButtonTableColumns(response.data.columns.map((c: any) => ({
|
||||
columnName: c.columnName || c.column_name,
|
||||
columnLabel: c.columnLabel || c.column_label || c.displayName,
|
||||
})));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("버튼 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
loadColumns();
|
||||
}, [config.buttonDataSource?.tableName]);
|
||||
|
||||
// 설정 업데이트 헬퍼
|
||||
const updateConfig = useCallback((updates: Partial<RelatedDataButtonsConfig>) => {
|
||||
onChange({ ...config, ...updates });
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateSourceMapping = useCallback((updates: Partial<RelatedDataButtonsConfig["sourceMapping"]>) => {
|
||||
onChange({
|
||||
...config,
|
||||
sourceMapping: { ...config.sourceMapping, ...updates },
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateHeaderDisplay = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["headerDisplay"]>>) => {
|
||||
onChange({
|
||||
...config,
|
||||
headerDisplay: { ...config.headerDisplay, ...updates } as any,
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateButtonDataSource = useCallback((updates: Partial<RelatedDataButtonsConfig["buttonDataSource"]>) => {
|
||||
onChange({
|
||||
...config,
|
||||
buttonDataSource: { ...config.buttonDataSource, ...updates },
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateButtonStyle = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["buttonStyle"]>>) => {
|
||||
onChange({
|
||||
...config,
|
||||
buttonStyle: { ...config.buttonStyle, ...updates },
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateAddButton = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["addButton"]>>) => {
|
||||
onChange({
|
||||
...config,
|
||||
addButton: { ...config.addButton, ...updates },
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const updateEvents = useCallback((updates: Partial<NonNullable<RelatedDataButtonsConfig["events"]>>) => {
|
||||
onChange({
|
||||
...config,
|
||||
events: { ...config.events, ...updates },
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
const tables = allTables.length > 0 ? allTables : propTables;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 소스 매핑 (좌측 패널 연결) */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">소스 테이블 (좌측 패널)</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">테이블</Label>
|
||||
<Popover open={sourceTableOpen} onOpenChange={setSourceTableOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" className="w-full justify-between">
|
||||
{config.sourceMapping?.sourceTable || "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." />
|
||||
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={`${table.displayName || ""} ${table.tableName}`}
|
||||
onSelect={() => {
|
||||
updateSourceMapping({ sourceTable: table.tableName });
|
||||
setSourceTableOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", config.sourceMapping?.sourceTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
||||
{table.displayName || table.tableName}
|
||||
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">필터 컬럼 (버튼 테이블 조회 시 사용)</Label>
|
||||
<Select
|
||||
value={config.sourceMapping?.sourceColumn || ""}
|
||||
onValueChange={(value) => updateSourceMapping({ sourceColumn: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 설정 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">헤더 표시</Label>
|
||||
<Switch
|
||||
checked={config.headerDisplay?.show !== false}
|
||||
onCheckedChange={(checked) => updateHeaderDisplay({ show: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.headerDisplay?.show !== false && (
|
||||
<div className="space-y-2 pl-2 border-l-2 border-gray-200">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">제목 컬럼</Label>
|
||||
<Select
|
||||
value={config.headerDisplay?.titleColumn || ""}
|
||||
onValueChange={(value) => updateHeaderDisplay({ titleColumn: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="제목 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">부제목 컬럼 (선택)</Label>
|
||||
<Select
|
||||
value={config.headerDisplay?.subtitleColumn || "__none__"}
|
||||
onValueChange={(value) => updateHeaderDisplay({ subtitleColumn: value === "__none__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="부제목 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">없음</SelectItem>
|
||||
{sourceTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 버튼 데이터 소스 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">버튼 데이터 소스</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">테이블</Label>
|
||||
<Popover open={buttonTableOpen} onOpenChange={setButtonTableOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" className="w-full justify-between">
|
||||
{config.buttonDataSource?.tableName || "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." />
|
||||
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={`${table.displayName || ""} ${table.tableName}`}
|
||||
onSelect={() => {
|
||||
updateButtonDataSource({ tableName: table.tableName });
|
||||
setButtonTableOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", config.buttonDataSource?.tableName === table.tableName ? "opacity-100" : "opacity-0")} />
|
||||
{table.displayName || table.tableName}
|
||||
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">필터 컬럼</Label>
|
||||
<Select
|
||||
value={config.buttonDataSource?.filterColumn || ""}
|
||||
onValueChange={(value) => updateButtonDataSource({ filterColumn: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{buttonTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">표시 컬럼</Label>
|
||||
<Select
|
||||
value={config.buttonDataSource?.displayColumn || ""}
|
||||
onValueChange={(value) => updateButtonDataSource({ displayColumn: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{buttonTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 스타일 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">버튼 스타일</Label>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">기본 스타일</Label>
|
||||
<Select
|
||||
value={config.buttonStyle?.variant || "outline"}
|
||||
onValueChange={(value: any) => updateButtonStyle({ variant: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="secondary">Secondary</SelectItem>
|
||||
<SelectItem value="ghost">Ghost</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">선택 시 스타일</Label>
|
||||
<Select
|
||||
value={config.buttonStyle?.activeVariant || "default"}
|
||||
onValueChange={(value: any) => updateButtonStyle({ activeVariant: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
<SelectItem value="outline">Outline</SelectItem>
|
||||
<SelectItem value="secondary">Secondary</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기본 표시 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">기본 버전 표시 컬럼</Label>
|
||||
<Select
|
||||
value={config.buttonStyle?.defaultIndicator?.column || "__none__"}
|
||||
onValueChange={(value) => updateButtonStyle({
|
||||
defaultIndicator: {
|
||||
...config.buttonStyle?.defaultIndicator,
|
||||
column: value === "__none__" ? "" : value,
|
||||
showStar: config.buttonStyle?.defaultIndicator?.showStar ?? true,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="없음" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">없음</SelectItem>
|
||||
{buttonTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{config.buttonStyle?.defaultIndicator?.column && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={config.buttonStyle?.defaultIndicator?.showStar ?? true}
|
||||
onCheckedChange={(checked) => updateButtonStyle({
|
||||
defaultIndicator: {
|
||||
...config.buttonStyle?.defaultIndicator,
|
||||
column: config.buttonStyle?.defaultIndicator?.column || "",
|
||||
showStar: checked,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<Label className="text-xs">별표 표시</Label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 이벤트 설정 (하위 테이블 연동) */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">하위 테이블 연동</Label>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">대상 테이블</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" className="w-full justify-between">
|
||||
{config.events?.targetTable || "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." />
|
||||
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{tables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={`${table.displayName || ""} ${table.tableName}`}
|
||||
onSelect={() => {
|
||||
updateEvents({ targetTable: table.tableName });
|
||||
}}
|
||||
>
|
||||
<Check className={cn("mr-2 h-4 w-4", config.events?.targetTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
||||
{table.displayName || table.tableName}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">필터 컬럼 (버튼 값 컬럼 → 대상 테이블 컬럼)</Label>
|
||||
<Input
|
||||
value={config.events?.targetFilterColumn || ""}
|
||||
onChange={(e) => updateEvents({ targetFilterColumn: e.target.value })}
|
||||
placeholder="예: routing_version_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 추가 버튼 설정 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-semibold">추가 버튼</Label>
|
||||
<Switch
|
||||
checked={config.addButton?.show ?? false}
|
||||
onCheckedChange={(checked) => updateAddButton({ show: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.addButton?.show && (
|
||||
<div className="space-y-2 pl-2 border-l-2 border-gray-200">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">버튼 라벨</Label>
|
||||
<Input
|
||||
value={config.addButton?.label || ""}
|
||||
onChange={(e) => updateAddButton({ label: e.target.value })}
|
||||
placeholder="+ 버전 추가"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">위치</Label>
|
||||
<Select
|
||||
value={config.addButton?.position || "header"}
|
||||
onValueChange={(value: any) => updateAddButton({ position: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="header">헤더 우측</SelectItem>
|
||||
<SelectItem value="inline">버튼들과 함께</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">모달 화면 ID</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.addButton?.modalScreenId || ""}
|
||||
onChange={(e) => updateAddButton({ modalScreenId: parseInt(e.target.value) || undefined })}
|
||||
placeholder="화면 ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 기타 설정 */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-semibold">기타 설정</Label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={config.autoSelectFirst ?? true}
|
||||
onCheckedChange={(checked) => updateConfig({ autoSelectFirst: checked })}
|
||||
/>
|
||||
<Label className="text-xs">첫 번째 항목 자동 선택</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">빈 상태 메시지</Label>
|
||||
<Input
|
||||
value={config.emptyMessage || ""}
|
||||
onChange={(e) => updateConfig({ emptyMessage: e.target.value })}
|
||||
placeholder="데이터가 없습니다"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedDataButtonsConfigPanel;
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { RelatedDataButtonsDefinition } from "./index";
|
||||
import { RelatedDataButtonsComponent } from "./RelatedDataButtonsComponent";
|
||||
|
||||
/**
|
||||
* RelatedDataButtons 렌더러
|
||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||
*/
|
||||
export class RelatedDataButtonsRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = RelatedDataButtonsDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
const { component } = this.props;
|
||||
|
||||
return (
|
||||
<RelatedDataButtonsComponent
|
||||
config={component?.config || RelatedDataButtonsDefinition.defaultConfig}
|
||||
className={component?.className}
|
||||
style={component?.style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 자동 등록 실행
|
||||
RelatedDataButtonsRenderer.registerSelf();
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { ComponentConfig } from "@/lib/registry/types";
|
||||
|
||||
export const relatedDataButtonsConfig: ComponentConfig = {
|
||||
id: "related-data-buttons",
|
||||
name: "연관 데이터 버튼",
|
||||
description: "좌측 패널에서 선택한 데이터를 기반으로 연관 테이블의 데이터를 버튼으로 표시합니다. 예: 품목 선택 → 라우팅 버전 버튼들",
|
||||
category: "data",
|
||||
webType: "container",
|
||||
version: "1.0.0",
|
||||
icon: "LayoutList",
|
||||
defaultConfig: {
|
||||
sourceMapping: {
|
||||
sourceTable: "",
|
||||
sourceColumn: "",
|
||||
},
|
||||
headerDisplay: {
|
||||
show: true,
|
||||
titleColumn: "",
|
||||
subtitleColumn: "",
|
||||
},
|
||||
buttonDataSource: {
|
||||
tableName: "",
|
||||
filterColumn: "",
|
||||
displayColumn: "",
|
||||
valueColumn: "id",
|
||||
orderColumn: "created_date",
|
||||
orderDirection: "ASC",
|
||||
},
|
||||
buttonStyle: {
|
||||
variant: "outline",
|
||||
activeVariant: "default",
|
||||
size: "default",
|
||||
defaultIndicator: {
|
||||
column: "",
|
||||
showStar: true,
|
||||
},
|
||||
},
|
||||
addButton: {
|
||||
show: false,
|
||||
label: "+ 버전 추가",
|
||||
position: "header",
|
||||
},
|
||||
events: {
|
||||
targetTable: "",
|
||||
targetFilterColumn: "",
|
||||
},
|
||||
autoSelectFirst: true,
|
||||
emptyMessage: "데이터가 없습니다",
|
||||
},
|
||||
configPanelComponent: "RelatedDataButtonsConfigPanel",
|
||||
rendererComponent: "RelatedDataButtonsRenderer",
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import { RelatedDataButtonsComponent } from "./RelatedDataButtonsComponent";
|
||||
import { RelatedDataButtonsConfigPanel } from "./RelatedDataButtonsConfigPanel";
|
||||
|
||||
/**
|
||||
* RelatedDataButtons 컴포넌트 정의
|
||||
* 좌측 패널에서 선택한 데이터를 기반으로 연관 테이블의 데이터를 버튼으로 표시
|
||||
*/
|
||||
export const RelatedDataButtonsDefinition = createComponentDefinition({
|
||||
id: "related-data-buttons",
|
||||
name: "연관 데이터 버튼",
|
||||
nameEng: "Related Data Buttons",
|
||||
description: "좌측 패널에서 선택한 데이터를 기반으로 연관 테이블의 데이터를 버튼으로 표시합니다. 예: 품목 선택 → 라우팅 버전 버튼들",
|
||||
category: ComponentCategory.DATA,
|
||||
webType: "container",
|
||||
component: RelatedDataButtonsComponent,
|
||||
defaultConfig: {
|
||||
sourceMapping: {
|
||||
sourceTable: "",
|
||||
sourceColumn: "",
|
||||
},
|
||||
headerDisplay: {
|
||||
show: true,
|
||||
titleColumn: "",
|
||||
subtitleColumn: "",
|
||||
},
|
||||
buttonDataSource: {
|
||||
tableName: "",
|
||||
filterColumn: "",
|
||||
displayColumn: "",
|
||||
valueColumn: "id",
|
||||
orderColumn: "created_date",
|
||||
orderDirection: "ASC",
|
||||
},
|
||||
buttonStyle: {
|
||||
variant: "outline",
|
||||
activeVariant: "default",
|
||||
size: "default",
|
||||
defaultIndicator: {
|
||||
column: "",
|
||||
showStar: true,
|
||||
},
|
||||
},
|
||||
addButton: {
|
||||
show: false,
|
||||
label: "+ 버전 추가",
|
||||
position: "header",
|
||||
},
|
||||
events: {
|
||||
targetTable: "",
|
||||
targetFilterColumn: "",
|
||||
},
|
||||
autoSelectFirst: true,
|
||||
emptyMessage: "데이터가 없습니다",
|
||||
},
|
||||
defaultSize: { width: 400, height: 120 },
|
||||
configPanel: RelatedDataButtonsConfigPanel,
|
||||
icon: "LayoutList",
|
||||
tags: ["버튼", "연관데이터", "마스터디테일", "라우팅"],
|
||||
version: "1.0.0",
|
||||
author: "개발팀",
|
||||
});
|
||||
|
||||
// 타입 내보내기
|
||||
export type { RelatedDataButtonsConfig, ButtonItem } from "./types";
|
||||
export { RelatedDataButtonsComponent } from "./RelatedDataButtonsComponent";
|
||||
export { RelatedDataButtonsConfigPanel } from "./RelatedDataButtonsConfigPanel";
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* RelatedDataButtons 컴포넌트 타입 정의
|
||||
*
|
||||
* 좌측 패널에서 선택한 데이터의 정보를 표시하고,
|
||||
* 연관 테이블의 데이터를 버튼으로 표시하는 컴포넌트
|
||||
*
|
||||
* 예시: 품목 선택 → 품목명/코드 표시 + 라우팅 버전 버튼들
|
||||
*/
|
||||
|
||||
/**
|
||||
* 헤더 표시 설정 (선택된 마스터 데이터 정보)
|
||||
*/
|
||||
export interface HeaderDisplayConfig {
|
||||
show?: boolean; // 헤더 표시 여부
|
||||
titleColumn: string; // 제목으로 표시할 컬럼 (예: item_name)
|
||||
subtitleColumn?: string; // 부제목으로 표시할 컬럼 (예: item_code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 버튼 데이터 소스 설정
|
||||
*/
|
||||
export interface ButtonDataSourceConfig {
|
||||
tableName: string; // 조회할 테이블명 (예: item_routing_version)
|
||||
filterColumn: string; // 필터링할 컬럼명 (예: item_code)
|
||||
displayColumn: string; // 버튼에 표시할 컬럼명 (예: version_name)
|
||||
valueColumn?: string; // 선택 시 전달할 값 컬럼 (기본: id)
|
||||
orderColumn?: string; // 정렬 컬럼
|
||||
orderDirection?: "ASC" | "DESC"; // 정렬 방향
|
||||
}
|
||||
|
||||
/**
|
||||
* 버튼 스타일 설정
|
||||
*/
|
||||
export interface ButtonStyleConfig {
|
||||
variant?: "default" | "outline" | "secondary" | "ghost";
|
||||
activeVariant?: "default" | "outline" | "secondary";
|
||||
size?: "sm" | "default" | "lg";
|
||||
// 기본 버전 표시 설정
|
||||
defaultIndicator?: {
|
||||
column: string; // 기본 여부 판단 컬럼 (예: is_default)
|
||||
value?: string; // 기본 값 (기본: "Y" 또는 true)
|
||||
showStar?: boolean; // 별표 아이콘 표시
|
||||
badgeText?: string; // 뱃지 텍스트 (예: "기본")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 추가 버튼 설정
|
||||
*/
|
||||
export interface AddButtonConfig {
|
||||
show?: boolean;
|
||||
label?: string; // 기본: "+ 버전 추가"
|
||||
modalScreenId?: number;
|
||||
position?: "header" | "inline"; // header: 헤더 우측, inline: 버튼들과 함께
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 설정 (하위 테이블 연동)
|
||||
*/
|
||||
export interface EventConfig {
|
||||
// 선택 시 하위 테이블 필터링
|
||||
targetTable?: string; // 필터링할 테이블명 (예: item_routing_detail)
|
||||
targetFilterColumn?: string; // 필터 컬럼명 (예: routing_version_id)
|
||||
// 커스텀 이벤트
|
||||
customEventName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 설정
|
||||
*/
|
||||
export interface RelatedDataButtonsConfig {
|
||||
// 소스 매핑 (좌측 패널 연결)
|
||||
sourceMapping: {
|
||||
sourceTable: string; // 좌측 패널 테이블명
|
||||
sourceColumn: string; // 필터에 사용할 컬럼 (예: item_code)
|
||||
};
|
||||
|
||||
// 헤더 표시 설정
|
||||
headerDisplay?: HeaderDisplayConfig;
|
||||
|
||||
// 버튼 데이터 소스
|
||||
buttonDataSource: ButtonDataSourceConfig;
|
||||
|
||||
// 버튼 스타일
|
||||
buttonStyle?: ButtonStyleConfig;
|
||||
|
||||
// 추가 버튼
|
||||
addButton?: AddButtonConfig;
|
||||
|
||||
// 이벤트 설정
|
||||
events?: EventConfig;
|
||||
|
||||
// 자동 선택
|
||||
autoSelectFirst?: boolean; // 첫 번째 (또는 기본) 항목 자동 선택
|
||||
|
||||
// 빈 상태 메시지
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 버튼 아이템 데이터
|
||||
*/
|
||||
export interface ButtonItem {
|
||||
id: string;
|
||||
displayText: string;
|
||||
value: string;
|
||||
isDefault: boolean;
|
||||
rawData: Record<string, any>;
|
||||
}
|
||||
|
|
@ -8,10 +8,11 @@
|
|||
import { WebType } from "./unified-core";
|
||||
|
||||
/**
|
||||
* 9개 핵심 입력 타입
|
||||
* 핵심 입력 타입
|
||||
*/
|
||||
export type BaseInputType =
|
||||
| "text" // 텍스트
|
||||
| "textarea" // 텍스트 에리어 (여러 줄)
|
||||
| "number" // 숫자
|
||||
| "date" // 날짜
|
||||
| "code" // 코드
|
||||
|
|
@ -34,16 +35,18 @@ export interface DetailTypeOption {
|
|||
* 입력 타입별 세부 타입 매핑
|
||||
*/
|
||||
export const INPUT_TYPE_DETAIL_TYPES: Record<BaseInputType, DetailTypeOption[]> = {
|
||||
// 텍스트 → text, email, tel, url, textarea, password
|
||||
// 텍스트 → text, email, tel, url, password
|
||||
text: [
|
||||
{ value: "text", label: "일반 텍스트", description: "기본 텍스트 입력" },
|
||||
{ value: "email", label: "이메일", description: "이메일 주소 입력" },
|
||||
{ value: "tel", label: "전화번호", description: "전화번호 입력" },
|
||||
{ value: "url", label: "URL", description: "웹사이트 주소 입력" },
|
||||
{ value: "textarea", label: "여러 줄 텍스트", description: "긴 텍스트 입력" },
|
||||
{ value: "password", label: "비밀번호", description: "비밀번호 입력 (마스킹)" },
|
||||
],
|
||||
|
||||
// 텍스트 에리어 → textarea
|
||||
textarea: [{ value: "textarea", label: "텍스트 에리어", description: "여러 줄 텍스트 입력" }],
|
||||
|
||||
// 숫자 → number, decimal, currency, percentage
|
||||
number: [
|
||||
{ value: "number", label: "정수", description: "정수 숫자 입력" },
|
||||
|
|
@ -102,8 +105,13 @@ export const INPUT_TYPE_DETAIL_TYPES: Record<BaseInputType, DetailTypeOption[]>
|
|||
* 웹타입에서 기본 입력 타입 추출
|
||||
*/
|
||||
export function getBaseInputType(webType: WebType): BaseInputType {
|
||||
// textarea (별도 타입으로 분리)
|
||||
if (webType === "textarea") {
|
||||
return "textarea";
|
||||
}
|
||||
|
||||
// text 계열
|
||||
if (["text", "email", "tel", "url", "textarea", "password"].includes(webType)) {
|
||||
if (["text", "email", "tel", "url", "password"].includes(webType)) {
|
||||
return "text";
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +175,7 @@ export function getDefaultDetailType(baseInputType: BaseInputType): WebType {
|
|||
*/
|
||||
export const BASE_INPUT_TYPE_OPTIONS: Array<{ value: BaseInputType; label: string; description: string }> = [
|
||||
{ value: "text", label: "텍스트", description: "텍스트 입력 필드" },
|
||||
{ value: "textarea", label: "텍스트 에리어", description: "여러 줄 텍스트 입력" },
|
||||
{ value: "number", label: "숫자", description: "숫자 입력 필드" },
|
||||
{ value: "date", label: "날짜", description: "날짜/시간 선택" },
|
||||
{ value: "code", label: "코드", description: "공통 코드 선택" },
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
* 주의: 이 파일을 수정할 때는 반드시 백엔드 타입도 함께 업데이트 해야 합니다.
|
||||
*/
|
||||
|
||||
// 9개 핵심 입력 타입
|
||||
// 핵심 입력 타입
|
||||
export type InputType =
|
||||
| "text" // 텍스트
|
||||
| "textarea" // 텍스트 에리어 (여러 줄 입력)
|
||||
| "number" // 숫자
|
||||
| "date" // 날짜
|
||||
| "code" // 코드
|
||||
|
|
@ -42,6 +43,13 @@ export const INPUT_TYPE_OPTIONS: InputTypeOption[] = [
|
|||
category: "basic",
|
||||
icon: "Type",
|
||||
},
|
||||
{
|
||||
value: "textarea",
|
||||
label: "텍스트 에리어",
|
||||
description: "여러 줄 텍스트 입력",
|
||||
category: "basic",
|
||||
icon: "AlignLeft",
|
||||
},
|
||||
{
|
||||
value: "number",
|
||||
label: "숫자",
|
||||
|
|
@ -130,6 +138,11 @@ export const INPUT_TYPE_DEFAULT_CONFIGS: Record<InputType, Record<string, any>>
|
|||
maxLength: 500,
|
||||
placeholder: "텍스트를 입력하세요",
|
||||
},
|
||||
textarea: {
|
||||
maxLength: 2000,
|
||||
rows: 4,
|
||||
placeholder: "내용을 입력하세요",
|
||||
},
|
||||
number: {
|
||||
min: 0,
|
||||
step: 1,
|
||||
|
|
@ -163,13 +176,17 @@ export const INPUT_TYPE_DEFAULT_CONFIGS: Record<InputType, Record<string, any>>
|
|||
radio: {
|
||||
inline: false,
|
||||
},
|
||||
image: {
|
||||
placeholder: "이미지를 선택하세요",
|
||||
accept: "image/*",
|
||||
},
|
||||
};
|
||||
|
||||
// 레거시 웹 타입 → 입력 타입 매핑
|
||||
export const WEB_TYPE_TO_INPUT_TYPE: Record<string, InputType> = {
|
||||
// 텍스트 관련
|
||||
text: "text",
|
||||
textarea: "text",
|
||||
textarea: "textarea",
|
||||
email: "text",
|
||||
tel: "text",
|
||||
url: "text",
|
||||
|
|
@ -204,6 +221,7 @@ export const WEB_TYPE_TO_INPUT_TYPE: Record<string, InputType> = {
|
|||
// 입력 타입 → 웹 타입 역매핑 (화면관리 시스템 호환용)
|
||||
export const INPUT_TYPE_TO_WEB_TYPE: Record<InputType, string> = {
|
||||
text: "text",
|
||||
textarea: "textarea",
|
||||
number: "number",
|
||||
date: "date",
|
||||
code: "code",
|
||||
|
|
@ -212,6 +230,7 @@ export const INPUT_TYPE_TO_WEB_TYPE: Record<InputType, string> = {
|
|||
select: "select",
|
||||
checkbox: "checkbox",
|
||||
radio: "radio",
|
||||
image: "image",
|
||||
};
|
||||
|
||||
// 입력 타입 변환 함수
|
||||
|
|
@ -226,6 +245,11 @@ export const INPUT_TYPE_VALIDATION_RULES: Record<InputType, Record<string, any>>
|
|||
trim: true,
|
||||
maxLength: 500,
|
||||
},
|
||||
textarea: {
|
||||
type: "string",
|
||||
trim: true,
|
||||
maxLength: 2000,
|
||||
},
|
||||
number: {
|
||||
type: "number",
|
||||
allowFloat: true,
|
||||
|
|
@ -258,4 +282,8 @@ export const INPUT_TYPE_VALIDATION_RULES: Record<InputType, Record<string, any>>
|
|||
type: "string",
|
||||
options: true,
|
||||
},
|
||||
image: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue