From 50410475c0492a29a894a1c0b3e47d6af07eda56 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 13 Nov 2025 18:06:11 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/screenManagementService.ts | 3 ++- backend-node/src/utils/dataFilterUtil.ts | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 87ee4544..1a9a393a 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; // 복사 대상 회사 코드 (최고 관리자 전용) } 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) => { From b3e217c1deaaf2107bd3694395fd3bf89782fdab Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 13 Nov 2025 18:09:54 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/services/screenManagementService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 1a9a393a..e7b6e806 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -2154,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(), ] ); @@ -2268,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, // 대상 회사 코드 전달 }); @@ -2284,7 +2284,7 @@ export class ScreenManagementService { screenCode: modalData.screenCode, description: "", companyCode: data.companyCode, - userId: data.userId, + createdBy: data.userId, targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달 }); From a3503c0b9fb9b2aa12f473e0f36c67b3b83ff1e7 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 10:26:09 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EB=B3=84=20polling=20=EC=84=A4=EC=A0=95=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/WidgetConfigSidebar.tsx | 78 +++++++++++++++---- .../dashboard/data-sources/MultiApiConfig.tsx | 24 ------ .../data-sources/MultiDatabaseConfig.tsx | 32 -------- frontend/components/admin/dashboard/types.ts | 4 +- .../dashboard/widgets/MapTestWidgetV2.tsx | 7 +- 5 files changed, 68 insertions(+), 77 deletions(-) diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index 7ca9684b..d126d8d9 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, } : {}), // 커스텀 메트릭 설정 @@ -356,6 +367,7 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge listConfig, chartConfig, customMetricConfig, + refreshInterval, onApply, onClose, ]); @@ -432,6 +444,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..bdf6b6b0 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -321,38 +321,6 @@ ORDER BY 하위부서수 DESC`, />
- {/* 마커 polling 간격 설정 (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/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 = From a491f083376f0501b2974f82d803f1344e033065 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 10:49:11 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=EC=A7=80=EB=8F=84=20=EC=9C=84=EC=A0=AF?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=EB=A7=88=EC=BB=A4=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20=EC=84=A0=ED=83=9D=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data-sources/MultiDatabaseConfig.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx index bdf6b6b0..9f5d21f3 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -321,6 +321,29 @@ ORDER BY 하위부서수 DESC`, />
+ {/* 마커 종류 선택 (MapTestWidgetV2 전용) */} +
+ + +

지도에 표시할 마커의 모양을 선택합니다

+
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
From 2eb8c3a61ba35e57f16dcd4396ad84ef8c897494 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 11:16:03 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/WidgetConfigSidebar.tsx | 4 + .../dashboard/widgets/ListTestWidget.tsx | 138 ++++++++++++------ 2 files changed, 97 insertions(+), 45 deletions(-) diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index d126d8d9..a18abf9a 100644 --- a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx @@ -352,6 +352,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", { subtype: element.subtype, + isMultiDataSourceWidget, + dataSources, + listConfig, + finalChartConfig, customMetricConfig, updatedElement, }); 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 ? (

데이터 소스를 연결해주세요 From 05273daa920131768077c867762c554606137be8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 11:35:16 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=B9=B4=EB=93=9C=20=EC=9C=84=EC=A0=AF=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/CustomMetricTestWidget.tsx | 25 ++++++++----------- .../dashboard/widgets/CustomMetricWidget.tsx | 23 ++++++----------- 2 files changed, 17 insertions(+), 31 deletions(-) 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}}
); From 02d4a3a3d38d82bd62e792b030d19b79744bba11 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Fri, 14 Nov 2025 12:10:10 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=81=AC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=9C=84=EC=A0=AF=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/widgets/RiskAlertTestWidget.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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}}
From 361cb56a1d3b90cbb9a6b1bf3bd9498a6d2ed242 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 14 Nov 2025 16:30:38 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=EA=B1=B0=EB=9E=98=EC=B2=98=EA=B4=80?= =?UTF-8?q?=EB=A6=AC-=ED=92=88=EB=AA=A9=EB=93=B1=EB=A1=9D=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomerItemMappingComponent.tsx | 258 +++++++++++++++++ .../CustomerItemMappingConfigPanel.tsx | 268 ++++++++++++++++++ .../CustomerItemMappingRenderer.tsx | 10 + .../components/customer-item-mapping/index.ts | 40 +++ .../components/customer-item-mapping/types.ts | 23 ++ frontend/lib/registry/components/index.ts | 1 + .../text-display/TextDisplayComponent.tsx | 4 +- .../lib/utils/getComponentConfigPanel.tsx | 2 + 8 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx create mode 100644 frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx create mode 100644 frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingRenderer.tsx create mode 100644 frontend/lib/registry/components/customer-item-mapping/index.ts create mode 100644 frontend/lib/registry/components/customer-item-mapping/types.ts 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..588c2ed6 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx @@ -0,0 +1,258 @@ +"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.showSearchArea && ( +
+
+
+
+ + + +
+

+ 검색 및 카테고리 필터 영역 +

+

+ 나중에 구현 예정 +

+
+
+
+ )} + + {/* 목록 헤더 */} +
+ 판매품목 목록 +
+ {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..555d4f57 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx @@ -0,0 +1,268 @@ +"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; +} + +export const CustomerItemMappingConfigPanel: React.FC< + CustomerItemMappingConfigPanelProps +> = ({ config, onChange }) => { + 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) => { + onChange({ + ...config, + selectedTable: tableName, + columns: [], // 테이블 변경 시 컬럼 초기화 + }); + }; + + const handleAddColumn = (columnName: string) => { + if (!config.columns.includes(columnName)) { + onChange({ + ...config, + columns: [...config.columns, columnName], + }); + } + }; + + const handleRemoveColumn = (columnName: string) => { + onChange({ + ...config, + columns: config.columns.filter((col) => col !== columnName), + }); + }; + + return ( +
+ {/* 테이블 선택 */} +
+ + +
+ + {/* 컬럼 설정 */} +
+ +
+ {/* 선택된 컬럼 목록 */} + {config.columns.length > 0 && ( +
+ {config.columns.map((col, index) => ( +
+ {col} + +
+ ))} +
+ )} + + {/* 컬럼 추가 */} + {availableColumns.length > 0 && ( + + )} +
+
+ + {/* 체크박스 설정 */} +
+ +
+ + + + + +
+
+ + {/* 검색 영역 설정 */} +
+ + + + + {config.showSearchArea && ( +
+ + + onChange({ + ...config, + searchAreaHeight: parseInt(e.target.value) || 80, + }) + } + min={60} + max={200} + className="h-8 text-xs" + /> +

+ 권장: 80-120px (나중에 검색창과 카테고리 필터 추가 예정) +

+
+ )} +
+ + {/* 빈 데이터 메시지 */} +
+ + + onChange({ ...config, emptyMessage: e.target.value }) + } + placeholder="데이터가 없습니다" + /> +
+ +
+ + + onChange({ ...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..1de07a05 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/index.ts @@ -0,0 +1,40 @@ +"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: false, + searchAreaHeight: 80, + 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..976bb568 --- /dev/null +++ b/frontend/lib/registry/components/customer-item-mapping/types.ts @@ -0,0 +1,23 @@ +export interface CustomerItemMappingConfig { + // 테이블 설정 + selectedTable?: string; + + // 컬럼 설정 + columns: string[]; // 표시할 컬럼 목록 + + // 체크박스 설정 + checkbox: { + enabled: boolean; + multiple: boolean; + selectAll: boolean; + }; + + // 검색/필터 영역 (나중에 추가할 공간) + showSearchArea?: boolean; + searchAreaHeight?: number; + + // 빈 데이터 메시지 + 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 Date: Fri, 14 Nov 2025 16:49:49 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=EA=B1=B0=EB=9E=98=EC=B2=98=EB=B3=84=20?= =?UTF-8?q?=ED=92=88=EB=AA=A9=EC=A0=95=EB=B3=B4=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EB=B0=94=20=EB=B0=8F=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomerItemMappingComponent.tsx | 60 +++--- .../CustomerItemMappingConfigPanel.tsx | 187 +++++++++++++++--- .../components/customer-item-mapping/index.ts | 8 +- .../components/customer-item-mapping/types.ts | 12 +- 4 files changed, 209 insertions(+), 58 deletions(-) diff --git a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx index 588c2ed6..a1cd73e5 100644 --- a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingComponent.tsx @@ -103,42 +103,48 @@ export const CustomerItemMappingComponent: React.FC

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

- {/* 검색/카테고리 영역 - 회색 배경 */} + {/* 검색/카테고리 영역 */} {finalConfig.showSearchArea && ( -
-
-
-
- - - +
+
+ {/* 검색 입력 */} +
+
+
-

- 검색 및 카테고리 필터 영역 -

-

- 나중에 구현 예정 -

+ + {/* 카테고리 필터 */} + {finalConfig.enableCategoryFilter && ( +
+ +
+ )}
)} diff --git a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx index 555d4f57..5aead062 100644 --- a/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx +++ b/frontend/lib/registry/components/customer-item-mapping/CustomerItemMappingConfigPanel.tsx @@ -19,11 +19,33 @@ 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 }) => { +> = ({ + 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([]); @@ -56,16 +78,18 @@ export const CustomerItemMappingConfigPanel: React.FC< }, [config.selectedTable]); const handleTableChange = (tableName: string) => { - onChange({ + const newConfig = { ...config, selectedTable: tableName, columns: [], // 테이블 변경 시 컬럼 초기화 - }); + }; + handleChange(newConfig); + propOnTableChange?.(tableName); }; const handleAddColumn = (columnName: string) => { if (!config.columns.includes(columnName)) { - onChange({ + handleChange({ ...config, columns: [...config.columns, columnName], }); @@ -73,7 +97,7 @@ export const CustomerItemMappingConfigPanel: React.FC< }; const handleRemoveColumn = (columnName: string) => { - onChange({ + handleChange({ ...config, columns: config.columns.filter((col) => col !== columnName), }); @@ -152,7 +176,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ + handleChange({ ...config, checkbox: { ...config.checkbox, @@ -168,7 +192,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ + handleChange({ ...config, checkbox: { ...config.checkbox, @@ -184,7 +208,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ + handleChange({ ...config, checkbox: { ...config.checkbox, @@ -198,6 +222,52 @@ export const CustomerItemMappingConfigPanel: React.FC<
+ {/* 헤더 설정 */} +
+ + + + {config.showCompanyName && availableColumns.length > 0 && ( +
+ + +

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

+
+ )} +
+ {/* 검색 영역 설정 */}
@@ -206,7 +276,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ + handleChange({ ...config, showSearchArea: checked as boolean, }) @@ -218,24 +288,83 @@ export const CustomerItemMappingConfigPanel: React.FC< {config.showSearchArea && ( -
- - - onChange({ - ...config, - searchAreaHeight: parseInt(e.target.value) || 80, - }) - } - min={60} - max={200} - className="h-8 text-xs" - /> -

- 권장: 80-120px (나중에 검색창과 카테고리 필터 추가 예정) -

+
+
+ + + 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 && ( + <> + + + + )} +
+ )} +
)}
@@ -246,7 +375,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ ...config, emptyMessage: e.target.value }) + handleChange({ ...config, emptyMessage: e.target.value }) } placeholder="데이터가 없습니다" /> @@ -257,7 +386,7 @@ export const CustomerItemMappingConfigPanel: React.FC< - onChange({ ...config, emptyDescription: e.target.value }) + handleChange({ ...config, emptyDescription: e.target.value }) } placeholder="품목 데이터가 추가되면 여기에 표시됩니다" /> diff --git a/frontend/lib/registry/components/customer-item-mapping/index.ts b/frontend/lib/registry/components/customer-item-mapping/index.ts index 1de07a05..a0d9d6c9 100644 --- a/frontend/lib/registry/components/customer-item-mapping/index.ts +++ b/frontend/lib/registry/components/customer-item-mapping/index.ts @@ -23,8 +23,14 @@ export const CustomerItemMappingDefinition = createComponentDefinition({ multiple: true, selectAll: true, }, - showSearchArea: false, + showSearchArea: true, // 기본적으로 검색 영역 표시 searchAreaHeight: 80, + searchPlaceholder: "품목코드, 품목명, 규격 검색", + enableCategoryFilter: true, // 기본적으로 카테고리 필터 표시 + categoryColumn: undefined, + categories: ["전체", "원자재", "반도체", "완제품"], + showCompanyName: false, + companyNameColumn: undefined, emptyMessage: "데이터가 없습니다", emptyDescription: "품목 데이터가 추가되면 여기에 표시됩니다", } as CustomerItemMappingConfig, diff --git a/frontend/lib/registry/components/customer-item-mapping/types.ts b/frontend/lib/registry/components/customer-item-mapping/types.ts index 976bb568..68382623 100644 --- a/frontend/lib/registry/components/customer-item-mapping/types.ts +++ b/frontend/lib/registry/components/customer-item-mapping/types.ts @@ -12,9 +12,19 @@ export interface CustomerItemMappingConfig { selectAll: boolean; }; - // 검색/필터 영역 (나중에 추가할 공간) + // 검색/필터 영역 showSearchArea?: boolean; searchAreaHeight?: number; + searchPlaceholder?: string; // 검색 플레이스홀더 + + // 카테고리 필터 + enableCategoryFilter?: boolean; // 카테고리 필터 활성화 + categoryColumn?: string; // 카테고리 데이터 컬럼명 + categories?: string[]; // 카테고리 목록 (예: ["전체", "원자재", "반도체", "완제품"]) + + // 헤더 설정 + showCompanyName?: boolean; // 회사명 표시 여부 + companyNameColumn?: string; // 회사명을 가져올 컬럼명 // 빈 데이터 메시지 emptyMessage?: string;