From a73b37f5580c0252ce00ae331407141c45d99fcc Mon Sep 17 00:00:00 2001 From: kjs Date: Tue, 16 Dec 2025 16:13:43 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A2=8C=EC=B8=A1=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=9A=B0=EC=B8=A1=EC=97=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=B0=8F=20=EB=B2=84=ED=8A=BC=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/api/data.ts | 9 +- frontend/lib/registry/components/index.ts | 3 + .../components/related-data-buttons/README.md | 162 +++++ .../RelatedDataButtonsComponent.tsx | 280 +++++++++ .../RelatedDataButtonsConfigPanel.tsx | 558 ++++++++++++++++++ .../RelatedDataButtonsRenderer.tsx | 29 + .../components/related-data-buttons/config.ts | 53 ++ .../components/related-data-buttons/index.ts | 71 +++ .../components/related-data-buttons/types.ts | 109 ++++ frontend/types/input-type-mapping.ts | 17 +- frontend/types/input-types.ts | 32 +- 11 files changed, 1316 insertions(+), 7 deletions(-) create mode 100644 frontend/lib/registry/components/related-data-buttons/README.md create mode 100644 frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx create mode 100644 frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel.tsx create mode 100644 frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsRenderer.tsx create mode 100644 frontend/lib/registry/components/related-data-buttons/config.ts create mode 100644 frontend/lib/registry/components/related-data-buttons/index.ts create mode 100644 frontend/lib/registry/components/related-data-buttons/types.ts diff --git a/frontend/lib/api/data.ts b/frontend/lib/api/data.ts index 8436dcf4..91c620d9 100644 --- a/frontend/lib/api/data.ts +++ b/frontend/lib/api/data.ts @@ -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[]; diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index b76a4542..e28e1755 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -85,6 +85,9 @@ import "./tax-invoice-list/TaxInvoiceListRenderer"; // 세금계산서 목록, // 🆕 메일 수신자 선택 컴포넌트 import "./mail-recipient-selector/MailRecipientSelectorRenderer"; // 내부 인원 선택 + 외부 이메일 입력 +// 🆕 연관 데이터 버튼 컴포넌트 +import "./related-data-buttons/RelatedDataButtonsRenderer"; // 좌측 선택 데이터 기반 연관 테이블 버튼 표시 + /** * 컴포넌트 초기화 함수 */ diff --git a/frontend/lib/registry/components/related-data-buttons/README.md b/frontend/lib/registry/components/related-data-buttons/README.md new file mode 100644 index 00000000..67212e63 --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/README.md @@ -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/` + diff --git a/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx new file mode 100644 index 00000000..768edfe9 --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsComponent.tsx @@ -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 = ({ + config, + className, + style, +}) => { + const [buttons, setButtons] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [loading, setLoading] = useState(false); + const [masterData, setMasterData] = useState | 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) => { + 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 ( +
+

+ 좌측에서 항목을 선택하세요 +

+
+ ); + } + + const headerConfig = config.headerDisplay; + const addButtonConfig = config.addButton; + + return ( +
+ {/* 헤더 영역 */} + {headerConfig?.show !== false && ( +
+
+ {/* 제목 (품목명 등) */} + {headerConfig?.titleColumn && masterData[headerConfig.titleColumn] && ( +

+ {masterData[headerConfig.titleColumn]} +

+ )} + {/* 부제목 (품목코드 등) */} + {headerConfig?.subtitleColumn && masterData[headerConfig.subtitleColumn] && ( +

+ {masterData[headerConfig.subtitleColumn]} +

+ )} +
+ + {/* 헤더 위치 추가 버튼 */} + {addButtonConfig?.show && addButtonConfig?.position === "header" && ( + + )} +
+ )} + + {/* 버튼 영역 */} +
+ {loading ? ( +
+ +
+ ) : buttons.length === 0 ? ( +
+

+ {config.emptyMessage || "데이터가 없습니다"} +

+ {/* 인라인 추가 버튼 (데이터 없을 때) */} + {addButtonConfig?.show && addButtonConfig?.position !== "header" && ( + + )} +
+ ) : ( +
+ {buttons.map((item) => ( + + ))} + + {/* 인라인 추가 버튼 */} + {addButtonConfig?.show && addButtonConfig?.position !== "header" && ( + + )} +
+ )} +
+
+ ); +}; + +export default RelatedDataButtonsComponent; diff --git a/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel.tsx b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel.tsx new file mode 100644 index 00000000..6c7d026a --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsConfigPanel.tsx @@ -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 = ({ + config, + onChange, + tables: propTables = [], +}) => { + const [allTables, setAllTables] = useState([]); + const [sourceTableColumns, setSourceTableColumns] = useState([]); + const [buttonTableColumns, setButtonTableColumns] = useState([]); + + // 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) => { + onChange({ ...config, ...updates }); + }, [config, onChange]); + + const updateSourceMapping = useCallback((updates: Partial) => { + onChange({ + ...config, + sourceMapping: { ...config.sourceMapping, ...updates }, + }); + }, [config, onChange]); + + const updateHeaderDisplay = useCallback((updates: Partial>) => { + onChange({ + ...config, + headerDisplay: { ...config.headerDisplay, ...updates } as any, + }); + }, [config, onChange]); + + const updateButtonDataSource = useCallback((updates: Partial) => { + onChange({ + ...config, + buttonDataSource: { ...config.buttonDataSource, ...updates }, + }); + }, [config, onChange]); + + const updateButtonStyle = useCallback((updates: Partial>) => { + onChange({ + ...config, + buttonStyle: { ...config.buttonStyle, ...updates }, + }); + }, [config, onChange]); + + const updateAddButton = useCallback((updates: Partial>) => { + onChange({ + ...config, + addButton: { ...config.addButton, ...updates }, + }); + }, [config, onChange]); + + const updateEvents = useCallback((updates: Partial>) => { + onChange({ + ...config, + events: { ...config.events, ...updates }, + }); + }, [config, onChange]); + + const tables = allTables.length > 0 ? allTables : propTables; + + return ( +
+ {/* 소스 매핑 (좌측 패널 연결) */} +
+ + +
+ + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((table) => ( + { + updateSourceMapping({ sourceTable: table.tableName }); + setSourceTableOpen(false); + }} + > + + {table.displayName || table.tableName} + {table.displayName && ({table.tableName})} + + ))} + + + + +
+ +
+ + +
+
+ + {/* 헤더 표시 설정 */} +
+
+ + updateHeaderDisplay({ show: checked })} + /> +
+ + {config.headerDisplay?.show !== false && ( +
+
+ + +
+ +
+ + +
+
+ )} +
+ + {/* 버튼 데이터 소스 */} +
+ + +
+ + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((table) => ( + { + updateButtonDataSource({ tableName: table.tableName }); + setButtonTableOpen(false); + }} + > + + {table.displayName || table.tableName} + {table.displayName && ({table.tableName})} + + ))} + + + + +
+ +
+
+ + +
+ +
+ + +
+
+
+ + {/* 버튼 스타일 */} +
+ + +
+
+ + +
+ +
+ + +
+
+ + {/* 기본 표시 설정 */} +
+ + +
+ + {config.buttonStyle?.defaultIndicator?.column && ( +
+
+ updateButtonStyle({ + defaultIndicator: { + ...config.buttonStyle?.defaultIndicator, + column: config.buttonStyle?.defaultIndicator?.column || "", + showStar: checked, + }, + })} + /> + +
+
+ )} +
+ + {/* 이벤트 설정 (하위 테이블 연동) */} +
+ + +
+ + + + + + + + + 테이블을 찾을 수 없습니다. + + {tables.map((table) => ( + { + updateEvents({ targetTable: table.tableName }); + }} + > + + {table.displayName || table.tableName} + + ))} + + + + +
+ +
+ + updateEvents({ targetFilterColumn: e.target.value })} + placeholder="예: routing_version_id" + /> +
+
+ + {/* 추가 버튼 설정 */} +
+
+ + updateAddButton({ show: checked })} + /> +
+ + {config.addButton?.show && ( +
+
+ + updateAddButton({ label: e.target.value })} + placeholder="+ 버전 추가" + /> +
+ +
+ + +
+ +
+ + updateAddButton({ modalScreenId: parseInt(e.target.value) || undefined })} + placeholder="화면 ID" + /> +
+
+ )} +
+ + {/* 기타 설정 */} +
+ + +
+ updateConfig({ autoSelectFirst: checked })} + /> + +
+ +
+ + updateConfig({ emptyMessage: e.target.value })} + placeholder="데이터가 없습니다" + /> +
+
+
+ ); +}; + +export default RelatedDataButtonsConfigPanel; + diff --git a/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsRenderer.tsx b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsRenderer.tsx new file mode 100644 index 00000000..07501e65 --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/RelatedDataButtonsRenderer.tsx @@ -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 ( + + ); + } +} + +// 자동 등록 실행 +RelatedDataButtonsRenderer.registerSelf(); diff --git a/frontend/lib/registry/components/related-data-buttons/config.ts b/frontend/lib/registry/components/related-data-buttons/config.ts new file mode 100644 index 00000000..093bf59f --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/config.ts @@ -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", +}; + diff --git a/frontend/lib/registry/components/related-data-buttons/index.ts b/frontend/lib/registry/components/related-data-buttons/index.ts new file mode 100644 index 00000000..0e5eae44 --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/index.ts @@ -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"; diff --git a/frontend/lib/registry/components/related-data-buttons/types.ts b/frontend/lib/registry/components/related-data-buttons/types.ts new file mode 100644 index 00000000..01585b6b --- /dev/null +++ b/frontend/lib/registry/components/related-data-buttons/types.ts @@ -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; +} diff --git a/frontend/types/input-type-mapping.ts b/frontend/types/input-type-mapping.ts index 30d87244..4bddfe5f 100644 --- a/frontend/types/input-type-mapping.ts +++ b/frontend/types/input-type-mapping.ts @@ -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 = { - // 텍스트 → 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 * 웹타입에서 기본 입력 타입 추출 */ 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: "공통 코드 선택" }, diff --git a/frontend/types/input-types.ts b/frontend/types/input-types.ts index e172e620..e3944cf1 100644 --- a/frontend/types/input-types.ts +++ b/frontend/types/input-types.ts @@ -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> 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> radio: { inline: false, }, + image: { + placeholder: "이미지를 선택하세요", + accept: "image/*", + }, }; // 레거시 웹 타입 → 입력 타입 매핑 export const WEB_TYPE_TO_INPUT_TYPE: Record = { // 텍스트 관련 text: "text", - textarea: "text", + textarea: "textarea", email: "text", tel: "text", url: "text", @@ -204,6 +221,7 @@ export const WEB_TYPE_TO_INPUT_TYPE: Record = { // 입력 타입 → 웹 타입 역매핑 (화면관리 시스템 호환용) export const INPUT_TYPE_TO_WEB_TYPE: Record = { text: "text", + textarea: "textarea", number: "number", date: "date", code: "code", @@ -212,6 +230,7 @@ export const INPUT_TYPE_TO_WEB_TYPE: Record = { select: "select", checkbox: "checkbox", radio: "radio", + image: "image", }; // 입력 타입 변환 함수 @@ -226,6 +245,11 @@ export const INPUT_TYPE_VALIDATION_RULES: Record> 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> type: "string", options: true, }, + image: { + type: "string", + required: false, + }, };