From 7f186c509f168bb9b98f95f802940ceff66025bd Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 16:44:42 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=91=EA=B3=84=20=EC=9C=84=EC=A0=AF=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EC=A7=84=ED=96=89=EC=83=81=ED=99=A9=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80:=20=EC=A7=91=EA=B3=84?= =?UTF-8?q?=20=EC=9C=84=EC=A0=AF=EC=9D=98=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=83=81=EC=84=B8=ED=95=9C=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=EC=9D=84=20=ED=8F=AC=ED=95=A8=ED=95=9C=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=83=88=EB=A1=9C=20=EC=9E=91=EC=84=B1=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EB=90=9C=20=EA=B8=B0=EB=8A=A5,=20=EB=AF=B8=EC=99=84=EB=A3=8C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5,=20=EC=82=AC=EC=9A=A9=20=EC=98=88?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=8F=20=EB=94=94=EB=B2=84=EA=B9=85=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9E=90=EB=93=A4=EC=9D=B4=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=9C=84=EC=A0=AF=EC=9D=84=20=EC=9D=B4=ED=95=B4?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20=ED=99=9C=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=8D=B0=20=EB=8F=84=EC=9B=80=EC=9D=84=20=EC=A3=BC=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.=20?= =?UTF-8?q?=EB=98=90=ED=95=9C,=20=EC=86=8C=EC=8A=A4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=A7=84=ED=96=89=20=EC=83=81=ED=99=A9=EC=9D=84=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=ED=95=98=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/집계위젯_개발진행상황.md | 210 ++++++++++++++++++ .../AggregationWidgetConfigPanel.tsx | 121 ++++++++-- .../components/v2-aggregation-widget/types.ts | 5 +- .../lib/utils/getComponentConfigPanel.tsx | 13 ++ 4 files changed, 331 insertions(+), 18 deletions(-) create mode 100644 docs/집계위젯_개발진행상황.md diff --git a/docs/집계위젯_개발진행상황.md b/docs/집계위젯_개발진행상황.md new file mode 100644 index 00000000..b01bd380 --- /dev/null +++ b/docs/집계위젯_개발진행상황.md @@ -0,0 +1,210 @@ +# 집계 위젯 (Aggregation Widget) 개발 진행상황 + +## 개요 +데이터의 합계, 평균, 개수, 최대값, 최소값 등을 집계하여 표시하는 위젯 + +## 파일 위치 +- **V2 버전**: `frontend/lib/registry/components/v2-aggregation-widget/` + - `index.ts` - 컴포넌트 정의 + - `types.ts` - 타입 정의 + - `AggregationWidgetComponent.tsx` - 메인 컴포넌트 + - `AggregationWidgetConfigPanel.tsx` - 설정 패널 + - `AggregationWidgetRenderer.tsx` - 렌더러 + +- **기존 버전**: `frontend/lib/registry/components/aggregation-widget/` + +--- + +## 완료된 기능 + +### 1. 기본 집계 기능 +- [x] 테이블 데이터 조회 및 집계 (SUM, AVG, COUNT, MAX, MIN) +- [x] 숫자형 컬럼 자동 감지 (`inputType` / `webType` 기반) +- [x] 집계 결과 포맷팅 (숫자, 통화, 퍼센트) +- [x] 가로/세로 레이아웃 지원 + +### 2. 데이터 소스 타입 +- [x] `table` - 테이블에서 직접 조회 +- [x] `component` - 다른 컴포넌트(리피터 등)에서 데이터 수신 +- [x] `selection` - 선택된 행 데이터로 집계 + +### 3. 필터 조건 +- [x] 필터 추가/삭제/활성화 UI +- [x] 연산자: =, !=, >, >=, <, <=, LIKE, IN, IS NULL, IS NOT NULL +- [x] 필터 결합 방식: AND / OR +- [x] 값 소스 타입: + - [x] `static` - 고정값 입력 + - [x] `formField` - 폼 필드에서 가져오기 + - [x] `selection` - 선택된 행에서 가져오기 (부분 완료) + - [x] `urlParam` - URL 파라미터에서 가져오기 +- [x] 카테고리 타입 컬럼 - 콤보박스로 값 선택 + +### 4. 자동 새로고침 +- [x] `autoRefresh` - 주기적 새로고침 +- [x] `refreshInterval` - 새로고침 간격 (초) +- [x] `refreshOnFormChange` - 폼 데이터 변경 시 새로고침 + +### 5. 스타일 설정 +- [x] 배경색, 테두리, 패딩 +- [x] 폰트 크기, 색상 +- [x] 라벨/아이콘 표시 여부 + +--- + +## 미완료 기능 + +### 1. 선택 데이터 필터 - 소스 컴포넌트 연동 (진행중) + +**현재 상태**: +- `FilterCondition`에 `sourceComponentId` 필드 추가됨 +- 설정 패널 UI에 소스 컴포넌트 선택 드롭다운 추가됨 +- 소스 컴포넌트 컬럼 로딩 함수 구현됨 + +**문제점**: +- `screenComponents`가 빈 배열로 전달되어 소스 컴포넌트 목록이 표시되지 않음 +- `allComponents` → `screenComponents` 변환이 `getComponentConfigPanel.tsx`에서 수행되지만, 실제 컴포넌트 목록이 비어있음 + +**해결 필요 사항**: +1. `UnifiedPropertiesPanel`에서 `allComponents`가 제대로 전달되는지 확인 +2. `getComponentConfigPanel.tsx`에서 `screenComponents` 변환 로직 디버깅 +3. 필터링 조건 확인 (table-list, v2-table-list, unified-repeater 등) + +**관련 코드**: +```typescript +// types.ts - FilterCondition +export interface FilterCondition { + // ... + sourceComponentId?: string; // 소스 컴포넌트 ID (NEW) + sourceColumnName?: string; // 소스 컬럼명 + // ... +} + +// AggregationWidgetConfigPanel.tsx +const selectableComponents = useMemo(() => { + return screenComponents.filter(comp => + comp.componentType === "table-list" || + comp.componentType === "v2-table-list" || + // ... + ); +}, [screenComponents]); +``` + +### 2. 런타임 선택 데이터 연동 + +**현재 상태**: +- `applyFilters` 함수에서 `selectedRows`를 사용하여 필터링 +- 하지만 특정 컴포넌트(`sourceComponentId`)의 선택 데이터를 가져오는 로직 미구현 + +**해결 필요 사항**: +1. 각 컴포넌트별 선택 데이터를 관리하는 글로벌 상태 또는 이벤트 시스템 구현 +2. `selectionChange` 이벤트에서 `componentId`별로 선택 데이터 저장 +3. `applyFilters`에서 `sourceComponentId`에 해당하는 선택 데이터 사용 + +**예상 구현**: +```typescript +// 컴포넌트별 선택 데이터 저장 (전역 상태) +const componentSelections = useRef>({}); + +// 이벤트 리스너 +window.addEventListener("selectionChange", (event) => { + const { componentId, selectedData } = event.detail; + componentSelections.current[componentId] = selectedData; +}); + +// 필터 적용 시 +case "selection": + const sourceData = componentSelections.current[filter.sourceComponentId]; + compareValue = sourceData?.[0]?.[filter.sourceColumnName]; + break; +``` + +### 3. 리피터 컨테이너 내부 집계 + +**시나리오**: +- 리피터 컨테이너 내부에 집계 위젯 배치 +- 각 반복 아이템별로 다른 집계 결과 표시 + +**현재 상태**: +- 리피터가 `formData`에 현재 아이템 데이터를 전달 +- 필터에서 `valueSourceType: "formField"`를 사용하면 현재 아이템 기준 필터링 가능 +- 테스트 미완료 + +**테스트 필요 케이스**: +1. 카테고리 리스트 리피터 + 집계 위젯 (해당 카테고리 상품 개수) +2. 주문 리스트 리피터 + 집계 위젯 (해당 주문의 상품 금액 합계) + +--- + +## 사용 예시 + +### 기본 사용 (테이블 전체 집계) +``` +데이터 소스: 테이블 → sales_order +집계 항목: + - 총 금액 (SUM of amount) + - 주문 건수 (COUNT) + - 평균 금액 (AVG of amount) +``` + +### 필터 사용 (조건부 집계) +``` +데이터 소스: 테이블 → sales_order +필터 조건: + - status = '완료' + - order_date >= 2026-01-01 +집계 항목: + - 완료 주문 금액 합계 +``` + +### 선택 데이터 연동 (목표) +``` +좌측: 품목 테이블 리스트 (item_mng) +우측: 집계 위젯 + +데이터 소스: 테이블 → sales_order +필터 조건: + - 컬럼: item_code + - 연산자: 같음 (=) + - 값 소스: 선택된 행 + - 소스 컴포넌트: 품목 리스트 + - 소스 컬럼: item_code + +→ 품목 선택 시 해당 품목의 수주 금액 합계 표시 +``` + +--- + +## 디버깅 로그 + +현재 설정 패널에 다음 로그가 추가되어 있음: +```typescript +console.log("[AggregationWidget] screenComponents:", screenComponents); +console.log("[AggregationWidget] selectableComponents:", filtered); +``` + +--- + +## 다음 단계 + +1. **소스 컴포넌트 목록 표시 문제 해결** + - `allComponents` 전달 경로 추적 + - `screenComponents` 변환 로직 확인 + +2. **컴포넌트별 선택 데이터 관리 구현** + - 글로벌 상태 또는 Context 사용 + - `selectionChange` 이벤트 표준화 + +3. **리피터 내부 집계 테스트** + - `formField` 필터로 현재 아이템 기준 집계 확인 + +4. **디버깅 로그 제거** + - 개발 완료 후 콘솔 로그 정리 + +--- + +## 관련 파일 + +- `frontend/lib/utils/getComponentConfigPanel.tsx` - `screenComponents` 변환 +- `frontend/components/screen/panels/UnifiedPropertiesPanel.tsx` - `allComponents` 전달 +- `frontend/components/screen/ScreenDesigner.tsx` - `layout.components` 전달 + diff --git a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx index bcdc1e32..4219d79d 100644 --- a/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel.tsx @@ -27,7 +27,7 @@ interface AggregationWidgetConfigPanelProps { onChange: (config: Partial) => void; screenTableName?: string; // 화면 내 컴포넌트 목록 (컴포넌트 연결용) - screenComponents?: Array<{ id: string; componentType: string; label?: string }>; + screenComponents?: Array<{ id: string; componentType: string; label?: string; tableName?: string }>; } /** @@ -138,9 +138,55 @@ export function AggregationWidgetConfigPanel({ // 카테고리 옵션 캐시 (categoryCode -> options) const [categoryOptionsCache, setCategoryOptionsCache] = useState>>({}); + // 소스 컴포넌트별 컬럼 캐시 (componentId -> columns) + const [sourceComponentColumnsCache, setSourceComponentColumnsCache] = useState>>({}); + // 데이터 소스 타입 (기본값: table) const dataSourceType = config.dataSourceType || "table"; + // 선택 가능한 데이터 소스 컴포넌트 (테이블 리스트 등) + const selectableComponents = useMemo(() => { + console.log("[AggregationWidget] screenComponents:", screenComponents); + const filtered = screenComponents.filter(comp => + comp.componentType === "table-list" || + comp.componentType === "v2-table-list" || + comp.componentType === "unified-repeater" || + comp.componentType === "v2-unified-repeater" || + comp.componentType === "repeat-container" || + comp.componentType === "v2-repeat-container" + ); + console.log("[AggregationWidget] selectableComponents:", filtered); + return filtered; + }, [screenComponents]); + + // 소스 컴포넌트 컬럼 로드 + const loadSourceComponentColumns = async (componentId: string) => { + // 이미 캐시에 있으면 스킵 + if (sourceComponentColumnsCache[componentId]) { + return; + } + + const sourceComp = screenComponents.find(c => c.id === componentId); + if (!sourceComp?.tableName) { + return; + } + + try { + const response = await tableManagementApi.getColumns(sourceComp.tableName); + const cols = (response.data?.columns || response.data || []).map((col: any) => ({ + columnName: col.column_name || col.columnName, + label: col.column_label || col.columnLabel || col.display_name || col.column_name || col.columnName, + })); + + setSourceComponentColumnsCache(prev => ({ + ...prev, + [componentId]: cols, + })); + } catch (error) { + console.error("소스 컴포넌트 컬럼 로드 실패:", error); + } + }; + // 실제 사용할 테이블 이름 계산 const targetTableName = useMemo(() => { if (config.useCustomTable && config.customTableName) { @@ -177,6 +223,17 @@ export function AggregationWidgetConfigPanel({ fetchTables(); }, []); + // 기존 필터의 소스 컴포넌트 컬럼 미리 로드 + useEffect(() => { + const filters = config.filters || []; + filters.forEach((filter) => { + if (filter.valueSourceType === "selection" && filter.sourceComponentId) { + loadSourceComponentColumns(filter.sourceComponentId); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.filters, screenComponents]); + // 테이블 컬럼 로드 useEffect(() => { const loadColumns = async () => { @@ -748,21 +805,53 @@ export function AggregationWidgetConfigPanel({ /> )} {filter.valueSourceType === "selection" && ( - +
+ {/* 소스 컴포넌트 선택 */} +
+ + +
+ {/* 소스 컬럼 선택 */} + {filter.sourceComponentId && ( +
+ + +
+ )} +
)} {filter.valueSourceType === "urlParam" && ( Promise> = { "map": () => import("@/lib/registry/components/map/MapConfigPanel"), "rack-structure": () => import("@/lib/registry/components/rack-structure/RackStructureConfigPanel"), "aggregation-widget": () => import("@/lib/registry/components/aggregation-widget/AggregationWidgetConfigPanel"), + "v2-aggregation-widget": () => import("@/lib/registry/components/v2-aggregation-widget/AggregationWidgetConfigPanel"), "numbering-rule": () => import("@/lib/registry/components/numbering-rule/NumberingRuleConfigPanel"), "category-manager": () => import("@/lib/registry/components/category-manager/CategoryManagerConfigPanel"), "universal-form-modal": () => import("@/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel"), @@ -489,6 +490,17 @@ export const DynamicComponentConfigPanel: React.FC = ); } + // 🆕 allComponents를 screenComponents 형태로 변환 (집계 위젯 등에서 사용) + const screenComponents = React.useMemo(() => { + if (!allComponents) return []; + return allComponents.map((comp: any) => ({ + id: comp.id, + componentType: comp.componentType || comp.type, + label: comp.label || comp.name || comp.id, + tableName: comp.componentConfig?.tableName || comp.tableName, + })); + }, [allComponents]); + return ( = menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달 allComponents={allComponents} // 🆕 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용) currentComponent={currentComponent} // 🆕 현재 컴포넌트 정보 + screenComponents={screenComponents} // 🆕 집계 위젯 등에서 사용하는 컴포넌트 목록 /> ); };