diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 87ee4544..e7b6e806 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -17,6 +17,7 @@ import { } from "../types/screen"; import { generateId } from "../utils/generateId"; +import logger from "../utils/logger"; // 화면 복사 요청 인터페이스 interface CopyScreenRequest { @@ -24,7 +25,7 @@ interface CopyScreenRequest { screenCode: string; description?: string; companyCode: string; // 요청한 사용자의 회사 코드 (인증용) - userId: string; + createdBy?: string; // 생성자 ID targetCompanyCode?: string; // 복사 대상 회사 코드 (최고 관리자 전용) } @@ -2153,9 +2154,9 @@ export class ScreenManagementService { targetCompanyCode, // 대상 회사 코드 사용 sourceScreen.table_name, sourceScreen.is_active, - copyData.userId, + copyData.createdBy, new Date(), - copyData.userId, + copyData.createdBy, new Date(), ] ); @@ -2267,7 +2268,7 @@ export class ScreenManagementService { screenCode: data.mainScreen.screenCode, description: data.mainScreen.description || "", companyCode: data.companyCode, - userId: data.userId, + createdBy: data.userId, targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달 }); @@ -2283,7 +2284,7 @@ export class ScreenManagementService { screenCode: modalData.screenCode, description: "", companyCode: data.companyCode, - userId: data.userId, + createdBy: data.userId, targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달 }); diff --git a/backend-node/src/utils/dataFilterUtil.ts b/backend-node/src/utils/dataFilterUtil.ts index 8c2732fb..d00861fb 100644 --- a/backend-node/src/utils/dataFilterUtil.ts +++ b/backend-node/src/utils/dataFilterUtil.ts @@ -26,16 +26,31 @@ export interface DataFilterConfig { */ export function buildDataFilterWhereClause( dataFilter: DataFilterConfig | undefined, - tableAlias?: string, + tableAliasOrStartIndex?: string | number, startParamIndex: number = 1 ): { whereClause: string; params: any[] } { if (!dataFilter || !dataFilter.enabled || !dataFilter.filters || dataFilter.filters.length === 0) { return { whereClause: "", params: [] }; } + // 파라미터 처리: 첫 번째 파라미터가 숫자면 startParamIndex, 문자열이면 tableAlias + let tableAlias: string | undefined; + let actualStartIndex: number; + + if (typeof tableAliasOrStartIndex === "number") { + actualStartIndex = tableAliasOrStartIndex; + tableAlias = undefined; + } else if (typeof tableAliasOrStartIndex === "string") { + tableAlias = tableAliasOrStartIndex; + actualStartIndex = startParamIndex; + } else { + actualStartIndex = startParamIndex; + tableAlias = undefined; + } + const conditions: string[] = []; const params: any[] = []; - let paramIndex = startParamIndex; + let paramIndex = actualStartIndex; // 테이블 별칭이 있으면 "alias."를 붙이고, 없으면 그냥 컬럼명만 const getColumnRef = (colName: string) => { diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index 7ca9684b..a18abf9a 100644 --- a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx @@ -17,6 +17,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { ApiConfig } from "./data-sources/ApiConfig"; import { QueryEditor } from "./QueryEditor"; @@ -146,6 +147,9 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge // 커스텀 메트릭 설정 const [customMetricConfig, setCustomMetricConfig] = useState({}); + // 자동 새로고침 간격 (지도 위젯용) + const [refreshInterval, setRefreshInterval] = useState(5); + // 사이드바 열릴 때 초기화 useEffect(() => { if (isOpen && element) { @@ -155,6 +159,8 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴 setDataSources(element.dataSources || element.chartConfig?.dataSources || []); setQueryResult(null); + // 자동 새로고침 간격 초기화 + setRefreshInterval(element.chartConfig?.refreshInterval ?? 5); // 리스트 위젯 설정 초기화 if (element.subtype === "list-v2" && element.listConfig) { @@ -290,6 +296,24 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge element.subtype === "custom-metric-v2" || element.subtype === "risk-alert-v2"; + // chartConfig 구성 (위젯 타입별로 다르게 처리) + let finalChartConfig = { ...chartConfig }; + + if (isMultiDataSourceWidget) { + finalChartConfig = { + ...finalChartConfig, + dataSources: dataSources, + }; + } + + // 지도 위젯인 경우 refreshInterval 추가 + if (element.subtype === "map-summary-v2") { + finalChartConfig = { + ...finalChartConfig, + refreshInterval, + }; + } + const updatedElement: DashboardElement = { ...element, customTitle: customTitle.trim() || undefined, @@ -302,8 +326,6 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge ...(isMultiDataSourceWidget ? { dataSources: dataSources, - // chartConfig에도 dataSources 포함 (일부 위젯은 chartConfig에서 읽음) - chartConfig: { ...chartConfig, dataSources: dataSources }, } : {}), } @@ -314,21 +336,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge listConfig, } : {}), - // 차트 설정 (차트 타입이거나 차트 기능이 있는 위젯) - ...(element.type === "chart" || - element.subtype === "chart" || - ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes(element.subtype) + // 차트 설정 (모든 위젯 공통) + ...(needsDataSource(element.subtype) ? { - // 다중 데이터 소스 위젯은 chartConfig에 dataSources 포함 (빈 배열도 허용 - 연결 해제) - chartConfig: isMultiDataSourceWidget - ? { ...chartConfig, dataSources: dataSources } - : chartConfig, - // 프론트엔드 호환성을 위해 dataSources도 element에 직접 포함 (빈 배열도 허용 - 연결 해제) - ...(isMultiDataSourceWidget - ? { - dataSources: dataSources, - } - : {}), + chartConfig: finalChartConfig, } : {}), // 커스텀 메트릭 설정 @@ -341,6 +352,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", { subtype: element.subtype, + isMultiDataSourceWidget, + dataSources, + listConfig, + finalChartConfig, customMetricConfig, updatedElement, }); @@ -356,6 +371,7 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge listConfig, chartConfig, customMetricConfig, + refreshInterval, onApply, onClose, ]); @@ -432,6 +448,40 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge + + {/* 자동 새로고침 설정 (지도 위젯 전용) */} + {element.subtype === "map-summary-v2" && ( +
+ + +

+ 위젯의 모든 데이터를 자동으로 갱신하는 주기를 설정합니다 +

+
+ )} diff --git a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx index c72cb18e..2aba31f8 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx @@ -530,30 +530,6 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M ))} - {/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */} -
- - -

- 마커 데이터를 자동으로 갱신하는 주기를 설정합니다 -

-
{/* 마커 종류 선택 (MapTestWidgetV2 전용) */}
diff --git a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx index 73b2ab4b..9f5d21f3 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -321,37 +321,28 @@ ORDER BY 하위부서수 DESC`, />
- {/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */} + {/* 마커 종류 선택 (MapTestWidgetV2 전용) */}
-
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */} diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index f5490dbf..e0fdb3a1 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -155,7 +155,6 @@ export interface ChartDataSource { jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results") // 공통 - refreshInterval?: number; // 자동 새로고침 (초, 0이면 수동) lastExecuted?: string; // 마지막 실행 시간 lastError?: string; // 마지막 오류 메시지 mapDisplayType?: "auto" | "marker" | "polygon"; // 지도 표시 방식 (auto: 자동, marker: 마커, polygon: 영역) @@ -184,6 +183,9 @@ export interface ChartConfig { // 다중 데이터 소스 (테스트 위젯용) dataSources?: ChartDataSource[]; // 여러 데이터 소스 (REST API + Database 혼합 가능) + // 위젯 레벨 설정 (MapTestWidgetV2용) + refreshInterval?: number; // 위젯 전체 자동 새로고침 간격 (초, 0이면 수동) + // 멀티 차트 설정 (ChartTestWidget용) chartType?: string; // 차트 타입 (line, bar, pie, etc.) mergeMode?: boolean; // 데이터 병합 모드 (여러 데이터 소스를 하나의 라인/바로 합침) diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index f7d97779..1b78801e 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -209,10 +209,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (loading) { return ( -
+
-

데이터 로딩 중...

+

데이터 로딩 중...

); @@ -220,12 +220,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (error) { return ( -
+
-

⚠️ {error}

+

⚠️ {error}

@@ -244,10 +244,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 설정이 없으면 안내 화면 if (!hasDataSource || !hasConfig) { return ( -
+
-

통계 카드

-
+

통계 카드

+

📊 단일 통계 위젯

  • • 데이터 소스에서 쿼리를 실행합니다
  • @@ -256,7 +256,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
  • • COUNT, SUM, AVG, MIN, MAX 지원
-
+

⚙️ 설정 방법

1. 데이터 탭에서 쿼리 실행

2. 필터 조건 추가 (선택사항)

@@ -274,7 +274,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 통계 카드 렌더링 (전체 크기 꽉 차게) return ( -
+
{/* 제목 */}
{config?.title || "통계"}
@@ -283,11 +283,6 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg {formattedValue} {config?.unit && {config.unit}}
- - {/* 필터 표시 (디버깅용, 작게) */} - {config?.filters && config.filters.length > 0 && ( -
필터: {config.filters.length}개 적용됨
- )}
); } diff --git a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx index 3f3538b4..fcd5593f 100644 --- a/frontend/components/dashboard/widgets/CustomMetricWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricWidget.tsx @@ -268,23 +268,14 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps) // 통계 카드 렌더링 return ( -
-
- {/* 제목 */} -
{config?.title || "통계"}
+
+ {/* 제목 */} +
{config?.title || "통계"}
- {/* 값 */} -
- {formattedValue} - {config?.unit && {config.unit}} -
- - {/* 필터 표시 (디버깅용, 작게) */} - {config?.filters && config.filters.length > 0 && ( -
- 필터: {config.filters.length}개 적용됨 -
- )} + {/* 값 */} +
+ {formattedValue} + {config?.unit && {config.unit}}
); diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index fdddb234..c46244b1 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -37,8 +37,19 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { - return element?.dataSources || element?.chartConfig?.dataSources; - }, [element?.dataSources, element?.chartConfig?.dataSources]); + // 다중 데이터 소스 우선 + const multiSources = element?.dataSources || element?.chartConfig?.dataSources; + if (multiSources && multiSources.length > 0) { + return multiSources; + } + + // 단일 데이터 소스 fallback (배열로 변환) + if (element?.dataSource) { + return [element.dataSource]; + } + + return []; + }, [element?.dataSources, element?.chartConfig?.dataSources, element?.dataSource]); // // console.log("📊 dataSources 확인:", { // hasDataSources: !!dataSources, @@ -58,6 +69,27 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { cardColumns: 3, }; + // visible 컬럼 설정 객체 배열 (field + label) + const visibleColumnConfigs = useMemo(() => { + if (config.columns && config.columns.length > 0 && typeof config.columns[0] === "object") { + return config.columns.filter((col: any) => col.visible !== false); + } + return []; + }, [config.columns]); + + // 표시할 컬럼 필드명 (데이터 접근용) + const displayColumns = useMemo(() => { + if (!data?.columns) return []; + + // 컬럼 설정이 있으면 field 사용 + if (visibleColumnConfigs.length > 0) { + return visibleColumnConfigs.map((col: any) => col.field); + } + + // 자동 모드: 모든 컬럼 표시 + return data.columns; + }, [data?.columns, visibleColumnConfigs]); + // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { @@ -313,50 +345,66 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const paginatedRows = data?.rows.slice(startIndex, endIndex) || []; // 테이블 뷰 - const renderTable = () => ( -
- - {config.showHeader && ( - - - {data?.columns.map((col) => ( - - {col} - - ))} - - - )} - - {paginatedRows.map((row, idx) => ( - - {data?.columns.map((col) => ( - - {String(row[col] ?? "")} - - ))} - - ))} - -
-
- ); + const renderTable = () => { + // 헤더명 가져오기 (label 우선, 없으면 field 그대로) + const getHeaderLabel = (field: string) => { + const colConfig = visibleColumnConfigs.find((col: any) => col.field === field); + return colConfig?.label || field; + }; + + return ( +
+ + {config.showHeader && ( + + + {displayColumns.map((field) => ( + + {getHeaderLabel(field)} + + ))} + + + )} + + {paginatedRows.map((row, idx) => ( + + {displayColumns.map((field) => ( + + {String(row[field] ?? "")} + + ))} + + ))} + +
+
+ ); + }; // 카드 뷰 - const renderCards = () => ( -
- {paginatedRows.map((row, idx) => ( - - {data?.columns.map((col) => ( -
- {col}: - {String(row[col] ?? "")} -
- ))} -
- ))} -
- ); + const renderCards = () => { + // 헤더명 가져오기 (label 우선, 없으면 field 그대로) + const getLabel = (field: string) => { + const colConfig = visibleColumnConfigs.find((col: any) => col.field === field); + return colConfig?.label || field; + }; + + return ( +
+ {paginatedRows.map((row, idx) => ( + + {displayColumns.map((field) => ( +
+ {getLabel(field)}: + {String(row[field] ?? "")} +
+ ))} +
+ ))} +
+ ); + }; return (
@@ -396,7 +444,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {

{error}

- ) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? ( + ) : !dataSources || dataSources.length === 0 ? (

데이터 소스를 연결해주세요 diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index dafc40fa..5df1663a 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -916,9 +916,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 즉시 첫 로드 (마커 데이터) loadMultipleDataSources(); - // 첫 번째 데이터 소스의 새로고침 간격 사용 (초) - const firstDataSource = dataSources[0]; - const refreshInterval = firstDataSource?.refreshInterval ?? 5; + // 위젯 레벨의 새로고침 간격 사용 (초) + const refreshInterval = element?.chartConfig?.refreshInterval ?? 5; // 0이면 자동 새로고침 비활성화 if (refreshInterval === 0) { @@ -933,7 +932,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { clearInterval(intervalId); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dataSources]); + }, [dataSources, element?.chartConfig?.refreshInterval]); // 타일맵 URL (chartConfig에서 가져오기) const tileMapUrl = diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 7b27bf69..8aa2e3e2 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -634,7 +634,26 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp )}

{alert.description}

- {new Date(alert.timestamp).toLocaleString("ko-KR")} + + {(() => { + const ts = String(alert.timestamp); + + // yyyyMMddHHmm 형식 감지 (예: 20251114 1000) + if (/^\d{12}$/.test(ts)) { + const year = ts.substring(0, 4); + const month = ts.substring(4, 6); + const day = ts.substring(6, 8); + const hour = ts.substring(8, 10); + const minute = ts.substring(10, 12); + const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:00`); + return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR"); + } + + // ISO 형식 또는 일반 날짜 형식 + const date = new Date(ts); + return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR"); + })()} + {alert.source && · {alert.source}}
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 41f8e966..8b6f94b1 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"; // 🆕 거래처별 품목정보 // 🆕 수주 등록 관련 컴포넌트들 import { AutocompleteSearchInputRenderer } from "./autocomplete-search-input/AutocompleteSearchInputRenderer"; 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 967b1fd5..9922e393 100644 --- a/frontend/lib/utils/getComponentConfigPanel.tsx +++ b/frontend/lib/utils/getComponentConfigPanel.tsx @@ -27,12 +27,15 @@ const CONFIG_PANEL_MAP: Record Promise> = { "repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"), "flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"), // 🆕 수주 등록 관련 컴포넌트들 - "autocomplete-search-input": () => import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"), + "autocomplete-search-input": () => + import("@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel"), "entity-search-input": () => import("@/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel"), "modal-repeater-table": () => import("@/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel"), - "order-registration-modal": () => import("@/lib/registry/components/order-registration-modal/OrderRegistrationModalConfigPanel"), + "order-registration-modal": () => + import("@/lib/registry/components/order-registration-modal/OrderRegistrationModalConfigPanel"), // 🆕 조건부 컨테이너 - "conditional-container": () => import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"), + "conditional-container": () => + import("@/lib/registry/components/conditional-container/ConditionalContainerConfigPanel"), }; // ConfigPanel 컴포넌트 캐시 @@ -62,6 +65,7 @@ export async function getComponentConfigPanel(componentId: string): Promise = // 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용 const isSimpleConfigPanel = [ "autocomplete-search-input", - "entity-search-input", + "entity-search-input", "modal-repeater-table", "order-registration-modal", - "conditional-container" + "conditional-container", ].includes(componentId); if (isSimpleConfigPanel) { - return ( - - ); + return ; } return (