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} // 🆕 집계 위젯 등에서 사용하는 컴포넌트 목록 /> ); };