Compare commits

...

3 Commits

8 changed files with 755 additions and 2 deletions

View File

@ -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<CustomerItemMappingComponentProps> = ({
component,
isDesignMode = false,
isSelected = false,
config,
className,
style,
onClick,
onDragStart,
onDragEnd,
}) => {
const finalConfig = {
...config,
...component.config,
} as CustomerItemMappingConfig;
const [data, setData] = useState<any[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<string>>(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 (
<div
className={cn("w-full h-full", className)}
style={componentStyle}
onClick={handleClick}
onDragStart={isDesignMode ? onDragStart : undefined}
onDragEnd={isDesignMode ? onDragEnd : undefined}
draggable={isDesignMode}
>
{/* 헤더 */}
<div className="w-full border-border bg-muted flex h-12 flex-shrink-0 items-center justify-between border-b px-4 sm:h-14 sm:px-6">
<h3 className="text-sm font-semibold sm:text-base">
- {finalConfig.selectedTable || "[테이블 선택]"}
{finalConfig.showCompanyName && finalConfig.companyNameColumn && (
<span className="text-muted-foreground ml-2 text-xs font-normal sm:text-sm">
| {finalConfig.companyNameColumn}
</span>
)}
</h3>
<button className="hover:bg-muted-foreground/10 rounded p-1">
<X className="h-4 w-4 sm:h-5 sm:w-5" />
</button>
</div>
{/* 검색/카테고리 영역 */}
{finalConfig.showSearchArea && (
<div className="w-full border-border bg-background flex-shrink-0 border-b p-3 sm:p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
{/* 검색 입력 */}
<div className="flex-1">
<div className="relative">
<input
type="text"
placeholder={finalConfig.searchPlaceholder || "품목코드, 품목명, 규격 검색"}
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring h-9 w-full rounded-md border px-3 py-1 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm"
disabled={isDesignMode}
/>
</div>
</div>
{/* 카테고리 필터 */}
{finalConfig.enableCategoryFilter && (
<div className="w-full sm:w-auto sm:min-w-[160px]">
<select
className="border-input bg-background ring-offset-background focus-visible:ring-ring h-9 w-full rounded-md border px-3 py-1 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm"
disabled={isDesignMode}
>
{(finalConfig.categories || ["전체"]).map((category, idx) => (
<option key={idx} value={category}>
{category}
</option>
))}
</select>
</div>
)}
</div>
</div>
)}
{/* 목록 헤더 */}
<div className="w-full border-border bg-muted/50 flex h-10 flex-shrink-0 items-center justify-between border-b px-4 sm:h-12 sm:px-6">
<span className="text-xs font-semibold sm:text-sm"> </span>
<div className="flex items-center gap-3 sm:gap-6">
{showCheckbox && finalConfig.checkbox?.selectAll && (
<label className="flex cursor-pointer items-center gap-2">
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} />
<span className="text-xs sm:text-sm"> </span>
</label>
)}
<span className="text-muted-foreground text-xs font-medium sm:text-sm">
: {selectedRows.size}
</span>
</div>
</div>
{/* 테이블 컨테이너 */}
<div className="flex w-full flex-1 flex-col overflow-hidden">
{/* 테이블 헤더 */}
{columns.length > 0 && (
<div className="border-border flex-shrink-0 border-b">
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: "100%" }}>
<thead>
<tr className="bg-muted/30 h-10 sm:h-12">
{showCheckbox && (
<th className="border-border w-12 border-r px-2 text-center sm:w-16 sm:px-3"></th>
)}
{columns.map((col, index) => (
<th
key={col}
className={cn(
"border-border text-foreground px-3 text-left text-xs font-semibold sm:px-6 sm:text-sm",
index < columns.length - 1 && "border-r"
)}
>
{col}
</th>
))}
</tr>
</thead>
</table>
</div>
</div>
)}
{/* 데이터 영역 */}
<div className="flex-1 overflow-y-auto">
{data.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-3 p-8 text-center sm:gap-4 sm:p-12">
<div className="bg-muted/50 flex h-16 w-16 items-center justify-center rounded-full sm:h-20 sm:w-20">
<svg
className="text-muted-foreground h-8 w-8 sm:h-10 sm:w-10"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
<div className="space-y-1 sm:space-y-2">
<p className="text-foreground text-base font-semibold sm:text-lg">
{finalConfig.emptyMessage || "데이터가 없습니다"}
</p>
<p className="text-muted-foreground text-xs sm:text-sm">
{finalConfig.emptyDescription || "품목 데이터가 추가되면 여기에 표시됩니다"}
</p>
</div>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: "100%" }}>
<tbody>
{data.map((row, index) => (
<tr key={index} className="hover:bg-muted/50 border-b transition-colors">
{showCheckbox && (
<td className="border-border w-12 border-r px-2 text-center sm:w-16 sm:px-3">
<Checkbox
checked={selectedRows.has(`row-${index}`)}
onCheckedChange={(checked) =>
handleRowSelection(`row-${index}`, checked as boolean)
}
/>
</td>
)}
{columns.map((col, colIndex) => (
<td
key={col}
className={cn(
"border-border px-3 py-2 text-xs sm:px-6 sm:py-3 sm:text-sm",
colIndex < columns.length - 1 && "border-r"
)}
>
{row[col] || "-"}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
};

View File

@ -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<any[]>([]);
const [availableColumns, setAvailableColumns] = useState<any[]>([]);
// 테이블 목록 로드
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 (
<div className="space-y-6 p-4">
{/* 테이블 선택 */}
<div className="space-y-2">
<Label> </Label>
<Select value={config.selectedTable} onValueChange={handleTableChange}>
<SelectTrigger>
<SelectValue placeholder="테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.displayName || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 컬럼 설정 */}
<div className="space-y-2">
<Label> </Label>
<div className="space-y-2">
{/* 선택된 컬럼 목록 */}
{config.columns.length > 0 && (
<div className="border-border space-y-1 rounded border p-2">
{config.columns.map((col, index) => (
<div
key={col}
className="bg-muted flex items-center justify-between rounded px-2 py-1"
>
<span className="text-sm">{col}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveColumn(col)}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
{/* 컬럼 추가 */}
{availableColumns.length > 0 && (
<Select onValueChange={handleAddColumn}>
<SelectTrigger>
<SelectValue placeholder="컬럼 추가" />
</SelectTrigger>
<SelectContent>
{availableColumns
.filter((col) => !config.columns.includes(col.columnName))
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</div>
{/* 체크박스 설정 */}
<div className="space-y-3">
<Label> </Label>
<div className="space-y-2">
<label className="flex items-center gap-2">
<Checkbox
checked={config.checkbox?.enabled !== false}
onCheckedChange={(checked) =>
handleChange({
...config,
checkbox: {
...config.checkbox,
enabled: checked as boolean,
},
})
}
/>
<span className="text-sm"> </span>
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={config.checkbox?.selectAll !== false}
onCheckedChange={(checked) =>
handleChange({
...config,
checkbox: {
...config.checkbox,
selectAll: checked as boolean,
},
})
}
/>
<span className="text-sm"> </span>
</label>
<label className="flex items-center gap-2">
<Checkbox
checked={config.checkbox?.multiple !== false}
onCheckedChange={(checked) =>
handleChange({
...config,
checkbox: {
...config.checkbox,
multiple: checked as boolean,
},
})
}
/>
<span className="text-sm"> </span>
</label>
</div>
</div>
{/* 헤더 설정 */}
<div className="space-y-3">
<Label> </Label>
<label className="flex items-center gap-2">
<Checkbox
checked={config.showCompanyName === true}
onCheckedChange={(checked) =>
handleChange({
...config,
showCompanyName: checked as boolean,
})
}
/>
<span className="text-sm font-medium"> </span>
</label>
{config.showCompanyName && availableColumns.length > 0 && (
<div className="space-y-2 pl-6">
<Label className="text-xs"> </Label>
<Select
value={config.companyNameColumn || ""}
onValueChange={(value) =>
handleChange({
...config,
companyNameColumn: value,
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground text-[10px]">
</p>
</div>
)}
</div>
{/* 검색 영역 설정 */}
<div className="space-y-3">
<Label>/ </Label>
<label className="flex items-center gap-2">
<Checkbox
checked={config.showSearchArea === true}
onCheckedChange={(checked) =>
handleChange({
...config,
showSearchArea: checked as boolean,
})
}
/>
<span className="text-sm font-medium">
/
</span>
</label>
{config.showSearchArea && (
<div className="space-y-3 pl-6">
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={config.searchPlaceholder || ""}
onChange={(e) =>
handleChange({
...config,
searchPlaceholder: e.target.value,
})
}
placeholder="품목코드, 품목명, 규격 검색"
className="h-8 text-xs"
/>
</div>
{/* 카테고리 필터 설정 */}
<div className="space-y-2">
<label className="flex items-center gap-2">
<Checkbox
checked={config.enableCategoryFilter === true}
onCheckedChange={(checked) =>
handleChange({
...config,
enableCategoryFilter: checked as boolean,
})
}
/>
<span className="text-xs font-medium"> </span>
</label>
{config.enableCategoryFilter && (
<div className="space-y-2 pl-6">
<Label className="text-xs"> ( )</Label>
<Input
value={(config.categories || []).join(", ")}
onChange={(e) =>
handleChange({
...config,
categories: e.target.value.split(",").map((c) => c.trim()).filter(Boolean),
})
}
placeholder="전체, 원자재, 반도체, 완제품"
className="h-8 text-xs"
/>
<p className="text-muted-foreground text-[10px]">
: 전체, , ,
</p>
{availableColumns.length > 0 && (
<>
<Label className="text-xs"> </Label>
<Select
value={config.categoryColumn || ""}
onValueChange={(value) =>
handleChange({
...config,
categoryColumn: value,
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
</div>
)}
</div>
</div>
)}
</div>
{/* 빈 데이터 메시지 */}
<div className="space-y-2">
<Label> </Label>
<Input
value={config.emptyMessage || ""}
onChange={(e) =>
handleChange({ ...config, emptyMessage: e.target.value })
}
placeholder="데이터가 없습니다"
/>
</div>
<div className="space-y-2">
<Label> </Label>
<Input
value={config.emptyDescription || ""}
onChange={(e) =>
handleChange({ ...config, emptyDescription: e.target.value })
}
placeholder="품목 데이터가 추가되면 여기에 표시됩니다"
/>
</div>
</div>
);
};

View File

@ -0,0 +1,10 @@
"use client";
import { ComponentRegistry } from "../../ComponentRegistry";
import { CustomerItemMappingDefinition } from "./index";
// 컴포넌트 자동 등록
ComponentRegistry.registerComponent(CustomerItemMappingDefinition);
console.log("✅ CustomerItemMapping 컴포넌트 등록 완료");

View File

@ -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";

View File

@ -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;
}

View File

@ -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"; // 🆕 거래처별 품목정보
/**
*

View File

@ -76,8 +76,8 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
: 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",
};

View File

@ -26,6 +26,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
"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<Reac
module[`${toPascalCase(componentId)}ConfigPanel`] ||
module.RepeaterConfigPanel || // repeater-field-group의 export명
module.FlowWidgetConfigPanel || // flow-widget의 export명
module.CustomerItemMappingConfigPanel || // customer-item-mapping의 export명
module.default;
if (!ConfigPanelComponent) {