diff --git a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx new file mode 100644 index 00000000..a1cd73e5 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx @@ -0,0 +1,264 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { CustomerItemMappingConfig } from "./types"; +import { Checkbox } from "@/components/ui/checkbox"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +export interface CustomerItemMappingComponentProps { + component: any; + isDesignMode?: boolean; + isSelected?: boolean; + isInteractive?: boolean; + config?: CustomerItemMappingConfig; + className?: string; + style?: React.CSSProperties; + onClick?: (e?: React.MouseEvent) => void; + onDragStart?: (e: React.DragEvent) => void; + onDragEnd?: (e: React.DragEvent) => void; +} + +export const CustomerItemMappingComponent: React.FC = ({ + component, + isDesignMode = false, + isSelected = false, + config, + className, + style, + onClick, + onDragStart, + onDragEnd, +}) => { + const finalConfig = { + ...config, + ...component.config, + } as CustomerItemMappingConfig; + + const [data, setData] = useState([]); + const [selectedRows, setSelectedRows] = useState>(new Set()); + const [isAllSelected, setIsAllSelected] = useState(false); + + // 데이터 로드 (실제 구현 시 API 호출) + useEffect(() => { + if (!isDesignMode && finalConfig.selectedTable) { + // TODO: API 호출로 데이터 로드 + setData([]); + } + }, [finalConfig.selectedTable, isDesignMode]); + + const handleSelectAll = (checked: boolean) => { + if (checked) { + const allIds = data.map((_, index) => `row-${index}`); + setSelectedRows(new Set(allIds)); + setIsAllSelected(true); + } else { + setSelectedRows(new Set()); + setIsAllSelected(false); + } + }; + + const handleRowSelection = (rowId: string, checked: boolean) => { + const newSelected = new Set(selectedRows); + if (checked) { + newSelected.add(rowId); + } else { + newSelected.delete(rowId); + } + setSelectedRows(newSelected); + setIsAllSelected(newSelected.size === data.length && data.length > 0); + }; + + const columns = finalConfig.columns || []; + const showCheckbox = finalConfig.checkbox?.enabled !== false; + + // 스타일 계산 + const componentStyle: React.CSSProperties = { + position: "relative", + width: "100%", + height: "100%", + display: "flex", + flexDirection: "column", + backgroundColor: "hsl(var(--background))", + overflow: "hidden", + boxSizing: "border-box", + }; + + // 이벤트 핸들러 + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onClick?.(); + }; + + return ( +
+ {/* 헤더 */} +
+

+ 품목 추가 - {finalConfig.selectedTable || "[테이블 선택]"} + {finalConfig.showCompanyName && finalConfig.companyNameColumn && ( + + | {finalConfig.companyNameColumn} + + )} +

+ +
+ + {/* 검색/카테고리 영역 */} + {finalConfig.showSearchArea && ( +
+
+ {/* 검색 입력 */} +
+
+ +
+
+ + {/* 카테고리 필터 */} + {finalConfig.enableCategoryFilter && ( +
+ +
+ )} +
+
+ )} + + {/* 목록 헤더 */} +
+ 판매품목 목록 +
+ {showCheckbox && finalConfig.checkbox?.selectAll && ( + + )} + + 선택: {selectedRows.size}개 + +
+
+ + {/* 테이블 컨테이너 */} +
+ {/* 테이블 헤더 */} + {columns.length > 0 && ( +
+
+ + + + {showCheckbox && ( + + )} + {columns.map((col, index) => ( + + ))} + + +
+ {col} +
+
+
+ )} + + {/* 데이터 영역 */} +
+ {data.length === 0 ? ( +
+
+ + + +
+
+

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

+

+ {finalConfig.emptyDescription || "품목 데이터가 추가되면 여기에 표시됩니다"} +

+
+
+ ) : ( +
+ + + {data.map((row, index) => ( + + {showCheckbox && ( + + )} + {columns.map((col, colIndex) => ( + + ))} + + ))} + +
+ + handleRowSelection(`row-${index}`, checked as boolean) + } + /> + + {row[col] || "-"} +
+
+ )} +
+
+
+ ); +}; + diff --git a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx new file mode 100644 index 00000000..5aead062 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx @@ -0,0 +1,397 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { CustomerItemMappingConfig } from "./types"; +import { tableTypeApi } from "@/lib/api/screen"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Plus, X } from "lucide-react"; + +export interface CustomerItemMappingConfigPanelProps { + config: CustomerItemMappingConfig; + onChange: (config: CustomerItemMappingConfig) => void; + onConfigChange?: (config: CustomerItemMappingConfig) => void; + screenTableName?: string; + tableColumns?: any[]; + tables?: any[]; + allTables?: any[]; + onTableChange?: (tableName: string) => void; + menuObjid?: number; +} + +export const CustomerItemMappingConfigPanel: React.FC< + CustomerItemMappingConfigPanelProps +> = ({ + config, + onChange, + onConfigChange, + screenTableName, + tableColumns: propTableColumns, + tables: propTables, + allTables, + onTableChange: propOnTableChange, + menuObjid, +}) => { + // onChange와 onConfigChange를 통합 + const handleChange = (newConfig: CustomerItemMappingConfig) => { + onChange?.(newConfig); + onConfigChange?.(newConfig); + }; + const [tables, setTables] = useState([]); + const [availableColumns, setAvailableColumns] = useState([]); + + // 테이블 목록 로드 + useEffect(() => { + const loadTables = async () => { + try { + const tableList = await tableTypeApi.getTables(); + setTables(tableList); + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + } + }; + loadTables(); + }, []); + + // 선택된 테이블의 컬럼 목록 로드 + useEffect(() => { + if (config.selectedTable) { + const loadColumns = async () => { + try { + const columns = await tableTypeApi.getColumns(config.selectedTable!); + setAvailableColumns(columns); + } catch (error) { + console.error("컬럼 목록 로드 실패:", error); + } + }; + loadColumns(); + } + }, [config.selectedTable]); + + const handleTableChange = (tableName: string) => { + const newConfig = { + ...config, + selectedTable: tableName, + columns: [], // 테이블 변경 시 컬럼 초기화 + }; + handleChange(newConfig); + propOnTableChange?.(tableName); + }; + + const handleAddColumn = (columnName: string) => { + if (!config.columns.includes(columnName)) { + handleChange({ + ...config, + columns: [...config.columns, columnName], + }); + } + }; + + const handleRemoveColumn = (columnName: string) => { + handleChange({ + ...config, + columns: config.columns.filter((col) => col !== columnName), + }); + }; + + return ( +
+ {/* 테이블 선택 */} +
+ + +
+ + {/* 컬럼 설정 */} +
+ +
+ {/* 선택된 컬럼 목록 */} + {config.columns.length > 0 && ( +
+ {config.columns.map((col, index) => ( +
+ {col} + +
+ ))} +
+ )} + + {/* 컬럼 추가 */} + {availableColumns.length > 0 && ( + + )} +
+
+ + {/* 체크박스 설정 */} +
+ +
+ + + + + +
+
+ + {/* 헤더 설정 */} +
+ + + + {config.showCompanyName && availableColumns.length > 0 && ( +
+ + +

+ 헤더에 표시할 회사명 데이터가 있는 컬럼을 선택하세요 +

+
+ )} +
+ + {/* 검색 영역 설정 */} +
+ + + + + {config.showSearchArea && ( +
+
+ + + handleChange({ + ...config, + searchPlaceholder: e.target.value, + }) + } + placeholder="품목코드, 품목명, 규격 검색" + className="h-8 text-xs" + /> +
+ + {/* 카테고리 필터 설정 */} +
+ + + {config.enableCategoryFilter && ( +
+ + + handleChange({ + ...config, + categories: e.target.value.split(",").map((c) => c.trim()).filter(Boolean), + }) + } + placeholder="전체, 원자재, 반도체, 완제품" + className="h-8 text-xs" + /> +

+ 예: 전체, 원자재, 반도체, 완제품 +

+ + {availableColumns.length > 0 && ( + <> + + + + )} +
+ )} +
+
+ )} +
+ + {/* 빈 데이터 메시지 */} +
+ + + handleChange({ ...config, emptyMessage: e.target.value }) + } + placeholder="데이터가 없습니다" + /> +
+ +
+ + + handleChange({ ...config, emptyDescription: e.target.value }) + } + placeholder="품목 데이터가 추가되면 여기에 표시됩니다" + /> +
+
+ ); +}; + diff --git a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingRenderer.tsx b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingRenderer.tsx new file mode 100644 index 00000000..9baebb9b --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingRenderer.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { ComponentRegistry } from "../../ComponentRegistry"; +import { CustomerItemMappingDefinition } from "./index"; + +// 컴포넌트 자동 등록 +ComponentRegistry.registerComponent(CustomerItemMappingDefinition); + +console.log("✅ CustomerItemMapping 컴포넌트 등록 완료"); + diff --git a/frontend/lib/registry/components/customer-item-mapping/index.ts b/frontend/lib/registry/components/customer-item-mapping/index.ts new file mode 100644 index 00000000..a0d9d6c9 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/index.ts @@ -0,0 +1,46 @@ +"use client"; + +import React from "react"; +import { createComponentDefinition } from "../../utils/createComponentDefinition"; +import { ComponentCategory } from "@/types/component"; +import { CustomerItemMappingComponent } from "./CustomerItemMappingComponent"; +import { CustomerItemMappingConfigPanel } from "./CustomerItemMappingConfigPanel"; +import { CustomerItemMappingConfig } from "./types"; + +export const CustomerItemMappingDefinition = createComponentDefinition({ + id: "customer-item-mapping", + name: "거래처별 품목정보", + nameEng: "Customer Item Mapping", + description: "거래처별 품목 정보를 표시하고 선택하는 컴포넌트", + category: ComponentCategory.DISPLAY, + webType: "text", + component: CustomerItemMappingComponent, + defaultConfig: { + selectedTable: undefined, + columns: [], + checkbox: { + enabled: true, + multiple: true, + selectAll: true, + }, + showSearchArea: true, // 기본적으로 검색 영역 표시 + searchAreaHeight: 80, + searchPlaceholder: "품목코드, 품목명, 규격 검색", + enableCategoryFilter: true, // 기본적으로 카테고리 필터 표시 + categoryColumn: undefined, + categories: ["전체", "원자재", "반도체", "완제품"], + showCompanyName: false, + companyNameColumn: undefined, + emptyMessage: "데이터가 없습니다", + emptyDescription: "품목 데이터가 추가되면 여기에 표시됩니다", + } as CustomerItemMappingConfig, + defaultSize: { width: 800, height: 600 }, + configPanel: CustomerItemMappingConfigPanel, + icon: "Package", + tags: ["거래처", "품목", "매핑", "목록"], + version: "1.0.0", + author: "개발팀", +}); + +export type { CustomerItemMappingConfig } from "./types"; + diff --git a/frontend/lib/registry/components/customer-item-mapping/types.ts b/frontend/lib/registry/components/customer-item-mapping/types.ts new file mode 100644 index 00000000..68382623 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/types.ts @@ -0,0 +1,33 @@ +export interface CustomerItemMappingConfig { + // 테이블 설정 + selectedTable?: string; + + // 컬럼 설정 + columns: string[]; // 표시할 컬럼 목록 + + // 체크박스 설정 + checkbox: { + enabled: boolean; + multiple: boolean; + selectAll: boolean; + }; + + // 검색/필터 영역 + showSearchArea?: boolean; + searchAreaHeight?: number; + searchPlaceholder?: string; // 검색 플레이스홀더 + + // 카테고리 필터 + enableCategoryFilter?: boolean; // 카테고리 필터 활성화 + categoryColumn?: string; // 카테고리 데이터 컬럼명 + categories?: string[]; // 카테고리 목록 (예: ["전체", "원자재", "반도체", "완제품"]) + + // 헤더 설정 + showCompanyName?: boolean; // 회사명 표시 여부 + companyNameColumn?: string; // 회사명을 가져올 컬럼명 + + // 빈 데이터 메시지 + emptyMessage?: string; + emptyDescription?: string; +} + diff --git a/frontend/lib/registry/components/index.ts b/frontend/lib/registry/components/index.ts index adc86414..17682ec8 100644 --- a/frontend/lib/registry/components/index.ts +++ b/frontend/lib/registry/components/index.ts @@ -43,6 +43,7 @@ import "./flow-widget/FlowWidgetRenderer"; import "./numbering-rule/NumberingRuleRenderer"; import "./category-manager/CategoryManagerRenderer"; import "./table-search-widget"; // 🆕 테이블 검색 필터 위젯 +import "./customer-item-mapping/CustomerItemMappingRenderer"; // 🆕 거래처별 품목정보 /** * 컴포넌트 초기화 함수 diff --git a/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx b/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx index a1e96a78..07ce3f34 100644 --- a/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx +++ b/frontend/lib/registry/components/text-display/TextDisplayComponent.tsx @@ -76,8 +76,8 @@ export const TextDisplayComponent: React.FC = ({ : componentConfig.textAlign === "right" ? "flex-end" : "flex-start", - wordBreak: "break-word", - overflow: "hidden", + whiteSpace: "nowrap", // ← 한 줄로 유지 // ← 넘치는 부분 숨김 + textOverflow: "ellipsis", // ← 넘치면 ... 표시 (선택사항) transition: "all 0.2s ease-in-out", boxShadow: "none", }; diff --git a/frontend/lib/utils/getComponentConfigPanel.tsx b/frontend/lib/utils/getComponentConfigPanel.tsx index b4af1632..d4b31d14 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -26,6 +26,7 @@ const CONFIG_PANEL_MAP: Record Promise> = { "split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"), "repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"), "flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"), + "customer-item-mapping": () => import("@/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel"), }; // ConfigPanel 컴포넌트 캐시 @@ -55,6 +56,7 @@ export async function getComponentConfigPanel(componentId: string): Promise