diff --git a/.changeset/fix-main-import-reference.md b/.changeset/fix-main-import-reference.md new file mode 100644 index 0000000..e25ed9c --- /dev/null +++ b/.changeset/fix-main-import-reference.md @@ -0,0 +1,7 @@ +--- +"vue-pivottable": patch +--- + +Fix main.ts import reference issue and optimize memory usage + +Fixed undefined SimpleApp reference to use App component in main.ts entry point. This resolves runtime errors and improves application startup reliability. \ No newline at end of file diff --git a/.changeset/fix-vue-pivottable-props-issue.md b/.changeset/fix-vue-pivottable-props-issue.md new file mode 100644 index 0000000..8dea59f --- /dev/null +++ b/.changeset/fix-vue-pivottable-props-issue.md @@ -0,0 +1,14 @@ +--- +"vue-pivottable": patch +--- + +Fix VuePivottable component props and type issues + +- Fix "Cannot read properties of undefined" error when using VuePivottable without VuePivottableUi +- Remove unnecessary composables export from main index to prevent VuePivottableUi code execution +- Make aggregatorName, renderers, rendererName optional in DefaultPropsType with proper defaults +- Add proper default values in VPivottable component +- Fix TSVExportRenderers to handle undefined aggregatorName +- Resolve Vue warning messages for missing required props + +This ensures VuePivottable can be used independently without requiring VuePivottableUi-specific props. \ No newline at end of file diff --git a/.github/workflows/integrate-develop.yml b/.github/workflows/integrate-develop.yml index 841ac8c..912ae3b 100644 --- a/.github/workflows/integrate-develop.yml +++ b/.github/workflows/integrate-develop.yml @@ -90,7 +90,7 @@ jobs: echo "- **Auto-generated**: This PR was automatically created by the integration workflow" echo "" echo "### ๐Ÿ“ Recent Commits" - echo "${{ steps.commit-info.outputs.commits }}" + echo "${{ steps.commit-info.outputs.commits }}" | sed 's/%0A/\n/g; s/%0D//g; s/%25/%/g' echo "" echo "### ๐Ÿ” What happens next?" echo "1. Review the changes in this PR" diff --git a/PIVOT_MODEL_FEATURE_DEVELOPMENT.md b/PIVOT_MODEL_FEATURE_DEVELOPMENT.md deleted file mode 100644 index 071591f..0000000 --- a/PIVOT_MODEL_FEATURE_DEVELOPMENT.md +++ /dev/null @@ -1,841 +0,0 @@ -# PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ๋ฌธ์„œ - -## ๊ฐœ์š” - -๋ณธ ๋ฌธ์„œ๋Š” vue3-pivottable์˜ PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ์— ๋Œ€ํ•œ ์ƒ์„ธํ•œ ๋ถ„์„๊ณผ ๊ตฌํ˜„ ๋ฐฉํ–ฅ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ VPivottableUi ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ UI๋ฅผ ํ†ตํ•ด ํ”ผ๋ฒ—ํ…Œ์ด๋ธ”์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ–ˆ์„ ๋•Œ, ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ด๋Ÿฌํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ถ”์ ํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค. - -## ํ˜„์žฌ ์ƒํ™ฉ ๋ถ„์„ - -### 1. ๊ธฐ์กด ์ฝ”๋“œ ํ˜„ํ™ฉ - -#### 1.1 pivotModel prop ์ •์˜ -- **์œ„์น˜**: `src/components/pivottable-ui/VPivottableUi.vue` 157๋ฒˆ์งธ ์ค„ -- **์ •์˜**: `pivotModel?: any` -- **๊ธฐ๋ณธ๊ฐ’**: `() => ({})` -- **์ƒํƒœ**: ์ •์˜๋˜์–ด ์žˆ์œผ๋‚˜ ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Œ - -#### 1.2 ์ƒํƒœ ๊ด€๋ฆฌ ๊ตฌ์กฐ -```typescript -// usePropsState composable์—์„œ ๊ด€๋ฆฌ๋˜๋Š” ์ƒํƒœ -interface PropsState { - rows: string[] // ํ–‰ ํ•„๋“œ - cols: string[] // ์—ด ํ•„๋“œ - vals: string[] // ๊ฐ’ ํ•„๋“œ - aggregatorName: string // ์ง‘๊ณ„ ํ•จ์ˆ˜๋ช… - rendererName: string // ๋ Œ๋”๋Ÿฌ๋ช… - valueFilter: Record // ํ•„ํ„ฐ ์ƒํƒœ - rowOrder: string // ํ–‰ ์ •๋ ฌ ์ˆœ์„œ - colOrder: string // ์—ด ์ •๋ ฌ ์ˆœ์„œ - heatmapMode: string // ํžˆํŠธ๋งต ๋ชจ๋“œ - // ๊ธฐํƒ€ props... -} -``` - -#### 1.3 ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ˜„ํ™ฉ -- **ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ๋“ค**: ๋ชจ๋‘ emit์„ ํ†ตํ•ด ์ƒ์œ„๋กœ ์ด๋ฒคํŠธ ์ „๋‹ฌ - - `VAggregatorCell`: `update:aggregator-name`, `update:vals` ๋“ฑ - - `VRendererCell`: `update:renderer-name` - - `VDragAndDropCell`: `update:dragged-attribute` -- **VPivottableUi**: emit ์ •์˜ ์—†์Œ โ†’ ์ƒํƒœ ๋ณ€๊ฒฝ์ด ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌ๋˜์ง€ ์•Š์Œ - -### 2. ๋ฌธ์ œ์  ์‹๋ณ„ - -#### 2.1 ํ•ต์‹ฌ ๋ฌธ์ œ -1. **๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„๋งŒ ์ง€์›**: ๋ถ€๋ชจ โ†’ ์ž์‹ ๋ฐฉํ–ฅ๋งŒ ๊ฐ€๋Šฅ -2. **์ƒํƒœ ๋ณ€๊ฒฝ ์ถ”์  ๋ถˆ๊ฐ€**: UI ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ถ€๋ชจ์—์„œ ๊ฐ์ง€ํ•  ์ˆ˜ ์—†์Œ -3. **์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์†์‹ค**: ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ, ํ•„ํ„ฐ๋ง, ์ •๋ ฌ ๋“ฑ์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ ค์ง€์ง€ ์•Š์Œ - -#### 2.2 ์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ๋ฌธ์ œ -```vue - - - - -``` - -## ๊ธฐ์ˆ ์  ๋ถ„์„ - -### 1. Vue 3 v-model ๊ตฌํ˜„ ์š”๊ตฌ์‚ฌํ•ญ - -#### 1.1 v-model ํŒจํ„ด -```typescript -// v-model:pivot-model์„ ์œ„ํ•œ emit ์ •์˜ -const emit = defineEmits<{ - 'update:pivotModel': [model: PivotModelInterface] -}>() -``` - -#### 1.2 ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ ํ๋ฆ„ -``` -๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ โ†’ VPivottableUi (props) - โ†‘ โ†“ - emit ์ด๋ฒคํŠธ โ† usePropsState (์ƒํƒœ ๋ณ€๊ฒฝ) -``` - -### 2. ํƒ€์ž… ์‹œ์Šคํ…œ ์„ค๊ณ„ - -#### 2.1 PivotModel ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜ -```typescript -interface PivotModelInterface { - // ํ•ต์‹ฌ ๊ตฌ์กฐ ํ•„๋“œ - rows: string[] // ํ–‰์œผ๋กœ ์‚ฌ์šฉํ•  ํ•„๋“œ๋“ค - cols: string[] // ์—ด๋กœ ์‚ฌ์šฉํ•  ํ•„๋“œ๋“ค - vals: string[] // ์ง‘๊ณ„ํ•  ๊ฐ’ ํ•„๋“œ๋“ค - - // ๋ Œ๋”๋ง ์˜ต์…˜ - aggregatorName: string // ์ง‘๊ณ„ ํ•จ์ˆ˜๋ช… (Sum, Count, Average ๋“ฑ) - rendererName: string // ๋ Œ๋”๋Ÿฌ๋ช… (Table, Table Heatmap ๋“ฑ) - heatmapMode?: string // ํžˆํŠธ๋งต ๋ชจ๋“œ ('full', 'row', 'col', '') - - // ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ - valueFilter: Record // ๊ฐ ํ•„๋“œ๋ณ„ ํ•„ํ„ฐ๋œ ๊ฐ’๋“ค - rowOrder: 'key_a_to_z' | 'value_a_to_z' | 'value_z_to_a' // ํ–‰ ์ •๋ ฌ ์ˆœ์„œ - colOrder: 'key_a_to_z' | 'value_a_to_z' | 'value_z_to_a' // ์—ด ์ •๋ ฌ ์ˆœ์„œ - - // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ - timestamp?: number // ๋งˆ์ง€๋ง‰ ๋ณ€๊ฒฝ ์‹œ๊ฐ„ - version?: string // ๋ชจ๋ธ ๋ฒ„์ „ -} -``` - -#### 2.2 ์ƒํƒœ ๋ณ€๊ฒฝ ํƒ€์ž… -```typescript -type PivotModelChangeEvent = { - type: 'field-move' | 'aggregator-change' | 'renderer-change' | 'filter-change' | 'sort-change' - field?: string - from?: string - to?: string - oldValue?: any - newValue?: any - timestamp: number -} -``` - -### 3. ๊ตฌํ˜„ ์•„ํ‚คํ…์ฒ˜ - -#### 3.1 ์ปดํฌ๋„ŒํŠธ ๋ ˆ์ด์–ด -``` -VPivottableUi (emit ์ถ”๊ฐ€) - โ†“ -usePropsState (emit ํ†ตํ•ฉ) - โ†“ -๊ฐœ๋ณ„ ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฉ”์„œ๋“œ๋“ค (emit ํ˜ธ์ถœ) -``` - -#### 3.2 ์ƒํƒœ ๋™๊ธฐํ™” ์ „๋žต -1. **์ฆ‰์‹œ ๋™๊ธฐํ™”**: ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ ์ฆ‰์‹œ emit -2. **debounce ์ ์šฉ**: ์—ฐ์†์ ์ธ ๋ณ€๊ฒฝ์— ๋Œ€ํ•ด ์ง€์—ฐ ์ฒ˜๋ฆฌ -3. **diff ์ฒดํฌ**: ์‹ค์ œ ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ emit - -## ๊ตฌํ˜„ ๊ณ„ํš - -### Phase 1: ๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ตฌ์ถ• (์šฐ์„ ์ˆœ์œ„: ๋†’์Œ) - -#### 1.1 ํƒ€์ž… ์ •์˜ ์ถ”๊ฐ€ -```typescript -// src/types/index.ts์— ์ถ”๊ฐ€ -export interface PivotModelInterface { - rows: string[] - cols: string[] - vals: string[] - aggregatorName: string - rendererName: string - valueFilter: Record - rowOrder: string - colOrder: string - heatmapMode?: string -} -``` - -#### 1.2 VPivottableUi emit ์ •์˜ -```typescript -// src/components/pivottable-ui/VPivottableUi.vue -const emit = defineEmits<{ - 'update:pivotModel': [model: PivotModelInterface] - 'change': [model: PivotModelInterface] -}>() -``` - -#### 1.3 usePropsState ์ˆ˜์ • -```typescript -// src/composables/usePropsState.ts -export function usePropsState( - initialProps: T, - emit?: (event: string, payload: any) => void // emit ํ•จ์ˆ˜ ์ถ”๊ฐ€ -) { - // ๊ธฐ์กด ์ฝ”๋“œ... - - const emitPivotModel = () => { - if (!emit) return - - const model: PivotModelInterface = { - rows: state.rows, - cols: state.cols, - vals: state.vals, - aggregatorName: state.aggregatorName, - rendererName: state.rendererName, - valueFilter: state.valueFilter, - rowOrder: state.rowOrder, - colOrder: state.colOrder, - heatmapMode: state.heatmapMode - } - - emit('update:pivotModel', model) - emit('change', model) - } - - // ๊ฐ update ๋ฉ”์„œ๋“œ์—์„œ emitPivotModel ํ˜ธ์ถœ - const onUpdateRendererName = (rendererName: string) => { - updateState('rendererName' as keyof T, rendererName) - // ๊ธฐ์กด ํžˆํŠธ๋งต ๋ชจ๋“œ ๋กœ์ง... - emitPivotModel() - } - - // ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋“ค๋„ ๋™์ผํ•˜๊ฒŒ ์ˆ˜์ •... -} -``` - -### Phase 2: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๊ตฌํ˜„ (์šฐ์„ ์ˆœ์œ„: ์ค‘๊ฐ„) - -#### 2.1 ์„ฑ๋Šฅ ์ตœ์ ํ™” -```typescript -// debounce๋ฅผ ํ†ตํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™” -import { debounce } from 'lodash-es' - -const emitPivotModelDebounced = debounce(emitPivotModel, 100) -``` - -#### 2.2 ๋ณ€๊ฒฝ ๊ฐ์ง€ ์ตœ์ ํ™” -```typescript -// ์ด์ „ ์ƒํƒœ์™€ ๋น„๊ตํ•˜์—ฌ ์‹ค์ œ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ๋งŒ emit -let previousModel: PivotModelInterface | null = null - -const emitPivotModel = () => { - const currentModel = buildPivotModel() - - if (previousModel && isEqual(previousModel, currentModel)) { - return // ๋ณ€๊ฒฝ์‚ฌํ•ญ ์—†์œผ๋ฉด emit ํ•˜์ง€ ์•Š์Œ - } - - previousModel = { ...currentModel } - emit('update:pivotModel', currentModel) - emit('change', currentModel) -} -``` - -#### 2.3 props์™€ ์ƒํƒœ ๋™๊ธฐํ™” -```typescript -// VPivottableUi.vue์—์„œ props ๋ณ€๊ฒฝ ๊ฐ์ง€ -watch( - () => props.pivotModel, - (newModel) => { - if (newModel && Object.keys(newModel).length > 0) { - updateMultiple({ - ...newModel - }) - } - }, - { deep: true, immediate: true } -) -``` - -### Phase 3: ๊ณ ๋„ํ™” ๊ธฐ๋Šฅ (์šฐ์„ ์ˆœ์œ„: ๋‚ฎ์Œ) - -#### 3.1 ์ƒํƒœ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ -```typescript -interface PivotModelHistory { - states: PivotModelInterface[] - currentIndex: number - - undo(): PivotModelInterface | null - redo(): PivotModelInterface | null - canUndo(): boolean - canRedo(): boolean -} -``` - -#### 3.2 ์ƒํƒœ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” -```typescript -class PivotModelSerializer { - static serialize(model: PivotModelInterface): string { - return JSON.stringify(model) - } - - static deserialize(json: string): PivotModelInterface { - return JSON.parse(json) - } - - static toUrlParams(model: PivotModelInterface): URLSearchParams { - // URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ณ€ํ™˜ - } - - static fromUrlParams(params: URLSearchParams): Partial { - // URL ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๋ณต์› - } -} -``` - -## ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฐ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค - -### 1. ๊ธฐ๋ณธ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค - -#### 1.1 ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ -```vue - - - -``` - -#### 1.2 ๋‹ค์ค‘ ํ”ผ๋ฒ—ํ…Œ์ด๋ธ” ๋™๊ธฐํ™” -```vue - - - -``` - -### 2. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค - -#### 2.1 ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -```typescript -describe('PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ', () => { - test('๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ์œผ๋กœ ํ•„๋“œ ์ด๋™ ์‹œ ๋ชจ๋ธ ์—…๋ฐ์ดํŠธ', async () => { - const { wrapper, pivotModel } = createVPivottableUiWrapper() - - // 'category' ํ•„๋“œ๋ฅผ unused์—์„œ rows๋กœ ๋“œ๋ž˜๊ทธ - await dragFieldToRows(wrapper, 'category') - - expect(pivotModel.value.rows).toContain('category') - }) - - test('์ง‘๊ณ„ ํ•จ์ˆ˜ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋ธ ์—…๋ฐ์ดํŠธ', async () => { - const { wrapper, pivotModel } = createVPivottableUiWrapper() - - await selectAggregator(wrapper, 'Average') - - expect(pivotModel.value.aggregatorName).toBe('Average') - }) - - test('๋ Œ๋”๋Ÿฌ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋ธ ์—…๋ฐ์ดํŠธ', async () => { - const { wrapper, pivotModel } = createVPivottableUiWrapper() - - await selectRenderer(wrapper, 'Table Heatmap') - - expect(pivotModel.value.rendererName).toBe('Table Heatmap') - expect(pivotModel.value.heatmapMode).toBe('full') - }) -}) -``` - -#### 2.2 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ -```typescript -describe('PivotModel ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ', () => { - test('๋ณต์žกํ•œ ์‚ฌ์šฉ์ž ์›Œํฌํ”Œ๋กœ์šฐ', async () => { - const { wrapper, pivotModel, emitSpy } = createVPivottableUiWrapper() - - // 1. ํ•„๋“œ ๋ฐฐ์น˜ - await dragFieldToRows(wrapper, 'region') - await dragFieldToCols(wrapper, 'quarter') - await dragFieldToVals(wrapper, 'sales') - - // 2. ์ง‘๊ณ„ ํ•จ์ˆ˜ ๋ณ€๊ฒฝ - await selectAggregator(wrapper, 'Average') - - // 3. ๋ Œ๋”๋Ÿฌ ๋ณ€๊ฒฝ - await selectRenderer(wrapper, 'Table Heatmap') - - // 4. ํ•„ํ„ฐ ์ ์šฉ - await applyFilter(wrapper, 'region', ['North', 'South']) - - // 5. ์ •๋ ฌ ๋ณ€๊ฒฝ - await changeRowOrder(wrapper, 'value_z_to_a') - - // ๋ชจ๋“  ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ๋ชจ๋ธ์— ๋ฐ˜์˜๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - expect(pivotModel.value).toEqual({ - rows: ['region'], - cols: ['quarter'], - vals: ['sales'], - aggregatorName: 'Average', - rendererName: 'Table Heatmap', - heatmapMode: 'full', - valueFilter: { region: ['North', 'South'] }, - rowOrder: 'value_z_to_a', - colOrder: 'key_a_to_z' - }) - - // emit์ด ์ ์ ˆํžˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - expect(emitSpy).toHaveBeenCalledTimes(5) - }) -}) -``` - -## ํ˜ธํ™˜์„ฑ ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ - -### 1. ๊ธฐ์กด ์ฝ”๋“œ์™€์˜ ํ˜ธํ™˜์„ฑ - -#### 1.1 Breaking Changes ์—†์Œ -- ๊ธฐ์กด props๋Š” ๋ชจ๋‘ ์œ ์ง€ -- ์ƒˆ๋กœ์šด emit ์ด๋ฒคํŠธ ์ถ”๊ฐ€๋งŒ์œผ๋กœ ๊ตฌํ˜„ -- ๊ธฐ์กด ์‚ฌ์šฉ์ž ์ฝ”๋“œ๋Š” ์ˆ˜์ • ์—†์ด ๋™์ž‘ - -#### 1.2 ์ ์ง„์  ๋„์ž… ๊ฐ€๋Šฅ -```vue - - - - - -``` - -### 2. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋„๊ตฌ - -#### 2.1 Props to Model ๋ณ€ํ™˜๊ธฐ -```typescript -function propsToModel(props: VPivottableUiProps): PivotModelInterface { - return { - rows: props.rows || [], - cols: props.cols || [], - vals: props.vals || [], - aggregatorName: props.aggregatorName || 'Count', - rendererName: props.rendererName || 'Table', - valueFilter: props.valueFilter || {}, - rowOrder: props.rowOrder || 'key_a_to_z', - colOrder: props.colOrder || 'key_a_to_z', - heatmapMode: props.heatmapMode || '' - } -} -``` - -#### 2.2 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ ๋ฌธ์„œ -```markdown -# PivotModel ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ - -## ๊ธฐ์กด ์ฝ”๋“œ -```vue - -``` - -## ์ƒˆ๋กœ์šด ์ฝ”๋“œ -```vue - - - -``` -``` - -## ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ - -### 1. ์ตœ์ ํ™” ์ „๋žต - -#### 1.1 ๋ถˆํ•„์š”ํ•œ ์žฌ๋ Œ๋”๋ง ๋ฐฉ์ง€ -```typescript -// shallow comparison์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ์—…๋ฐ์ดํŠธ ๋ฐฉ์ง€ -const shouldEmit = (oldModel: PivotModelInterface, newModel: PivotModelInterface): boolean => { - const keys: (keyof PivotModelInterface)[] = [ - 'rows', 'cols', 'vals', 'aggregatorName', 'rendererName', - 'rowOrder', 'colOrder', 'heatmapMode' - ] - - for (const key of keys) { - if (key === 'valueFilter') { - if (!isEqual(oldModel[key], newModel[key])) return true - } else if (Array.isArray(oldModel[key])) { - if (!arraysEqual(oldModel[key] as string[], newModel[key] as string[])) return true - } else { - if (oldModel[key] !== newModel[key]) return true - } - } - - return false -} -``` - -#### 1.2 ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ตœ์ ํ™” -- ์ด์ „ ์ƒํƒœ๋Š” shallow copy๋งŒ ์œ ์ง€ -- ํ•„์š”์‹œ์—๋งŒ deep clone ์ˆ˜ํ–‰ -- ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํžˆ์Šคํ† ๋ฆฌ๋Š” ์ž๋™ ์ •๋ฆฌ - -#### 1.3 ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ -```typescript -// ๋Œ€์šฉ๋Ÿ‰ ํ•„ํ„ฐ ์ฒ˜๋ฆฌ์‹œ ์„ฑ๋Šฅ ์ตœ์ ํ™” -const optimizedValueFilter = computed(() => { - const filter = state.valueFilter - - // ๋นˆ ํ•„ํ„ฐ๋Š” ์ œ๊ฑฐ - return Object.fromEntries( - Object.entries(filter).filter(([key, values]) => - values && values.length > 0 - ) - ) -}) -``` - -### 2. ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ - -#### 2.1 ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ •๋ฆฌ -```typescript -// composable์—์„œ cleanup ํ•จ์ˆ˜ ์ œ๊ณต -export function usePropsState(initialProps, emit) { - // ... ๊ธฐ์กด ์ฝ”๋“œ - - const cleanup = () => { - // debounced ํ•จ์ˆ˜ ์ทจ์†Œ - emitPivotModelDebounced.cancel() - - // ์ƒํƒœ ์ฐธ์กฐ ํ•ด์ œ - previousModel = null - } - - return { - // ... ๊ธฐ์กด ๋ฐ˜ํ™˜๊ฐ’ - cleanup - } -} - -// VPivottableUi.vue์—์„œ cleanup ํ˜ธ์ถœ -onUnmounted(() => { - cleanup?.() -}) -``` - -## ๋ฌธ์„œํ™” ๋ฐ ์˜ˆ์ œ - -### 1. API ๋ฌธ์„œ - -#### 1.1 Props ๋ฌธ์„œ -```typescript -interface VPivottableUiProps { - // ๊ธฐ์กด props๋“ค... - - /** - * ํ”ผ๋ฒ—ํ…Œ์ด๋ธ”์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ชจ๋ธ ๊ฐ์ฒด - * v-model:pivot-model๋กœ ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ ๊ฐ€๋Šฅ - * @since v1.2.0 - */ - pivotModel?: PivotModelInterface -} -``` - -#### 1.2 Events ๋ฌธ์„œ -```typescript -interface VPivottableUiEmits { - /** - * ํ”ผ๋ฒ— ๋ชจ๋ธ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐœ์ƒ - * v-model:pivot-model๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ - * @param model ๋ณ€๊ฒฝ๋œ ํ”ผ๋ฒ— ๋ชจ๋ธ - * @since v1.2.0 - */ - 'update:pivotModel': [model: PivotModelInterface] - - /** - * ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ ํ”ผ๋ฒ— ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐœ์ƒ - * @param model ๋ณ€๊ฒฝ๋œ ํ”ผ๋ฒ— ๋ชจ๋ธ - * @since v1.2.0 - */ - 'change': [model: PivotModelInterface] -} -``` - -### 2. ์˜ˆ์ œ ์ฝ”๋“œ - -#### 2.1 ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ• -```vue - - - -``` - -#### 2.2 ๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ• -```vue - - - -``` - -## ํ’ˆ์งˆ ๋ณด์ฆ - -### 1. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชฉํ‘œ -- ๋‹จ์œ„ ํ…Œ์ŠคํŠธ: 90% ์ด์ƒ -- ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ์ฃผ์š” ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค 100% ์ปค๋ฒ„ -- E2E ํ…Œ์ŠคํŠธ: ํ•ต์‹ฌ ์›Œํฌํ”Œ๋กœ์šฐ ์ปค๋ฒ„ - -### 2. ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ -- 1000๊ฐœ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ์—์„œ ์ƒํƒœ ๋ณ€๊ฒฝ ์‘๋‹ต์‹œ๊ฐ„ < 100ms -- 10000๊ฐœ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ์—์„œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€ < 10MB -- ์—ฐ์†์ ์ธ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ์ž‘์—…์—์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์—†์Œ - -### 3. ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ - -## ๋ฆด๋ฆฌ์ฆˆ ๊ณ„ํš - -### Version 1.2.0 (Major Feature Release) -- **๋ชฉํ‘œ ์ผ์ •**: ๊ฐœ๋ฐœ ์™„๋ฃŒ ํ›„ 2์ฃผ ๋‚ด -- **์ฃผ์š” ๊ธฐ๋Šฅ**: - - PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ - - TypeScript ํƒ€์ž… ์ •์˜ - - ๊ธฐ๋ณธ emit ์ด๋ฒคํŠธ ์ง€์› - - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ - -### Version 1.2.1 (Performance & Stability) -- **๋ชฉํ‘œ ์ผ์ •**: 1.2.0 ๋ฆด๋ฆฌ์ฆˆ ํ›„ 1์ฃผ ๋‚ด -- **๊ฐœ์„  ์‚ฌํ•ญ**: - - ์„ฑ๋Šฅ ์ตœ์ ํ™” - - ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ - - ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„  - -### Version 1.3.0 (Advanced Features) -- **๋ชฉํ‘œ ์ผ์ •**: 1.2.1 ๋ฆด๋ฆฌ์ฆˆ ํ›„ 1๋‹ฌ ๋‚ด -- **๊ณ ๊ธ‰ ๊ธฐ๋Šฅ**: - - ์ƒํƒœ ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ - - ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” - - URL ์ƒํƒœ ๋™๊ธฐํ™” - -## ๊ฒฐ๋ก  - -๋ณธ PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ ๊ธฐ๋Šฅ์€ vue3-pivottable์˜ ์‚ฌ์šฉ์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. - -### ์ฃผ์š” ์ด์ : -1. **๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ**: ์ƒํƒœ ๋ณ€๊ฒฝ ์ถ”์  ๋ฐ ๊ด€๋ฆฌ ์šฉ์ด -2. **์‹ค์šฉ์„ฑ ์ฆ๋Œ€**: ์ƒํƒœ ์ €์žฅ/๋ณต์›, ๋‹ค์ค‘ ํ”ผ๋ฒ—ํ…Œ์ด๋ธ” ๋™๊ธฐํ™” ๋“ฑ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ง€์› -3. **Vue 3 ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ**: v-model ํŒจํ„ด์„ ํ†ตํ•œ ์ž์—ฐ์Šค๋Ÿฌ์šด Vue 3 ๊ฐœ๋ฐœ ๊ฒฝํ—˜ - -### ๊ธฐ์ˆ ์  ์•ˆ์ •์„ฑ: -- ๊ธฐ์กด API์™€ 100% ํ˜ธํ™˜ -- ์ ์ง„์  ๋„์ž… ๊ฐ€๋Šฅ -- ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ์•ˆ์ „์„ฑ ๋ณด์žฅ - -์ด ๊ธฐ๋Šฅ์˜ ๊ตฌํ˜„์„ ํ†ตํ•ด vue3-pivottable์€ ๋‹จ์ˆœํ•œ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•œ ์™„์ „ํ•œ ํ”ผ๋ฒ—ํ…Œ์ด๋ธ” ์†”๋ฃจ์…˜์œผ๋กœ ๋ฐœ์ „ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. \ No newline at end of file diff --git a/AI_USAGE_GUIDELINES.md b/docs/AI_USAGE_GUIDELINES.md similarity index 100% rename from AI_USAGE_GUIDELINES.md rename to docs/AI_USAGE_GUIDELINES.md diff --git a/RELEASE_STRATEGY.ko.md b/docs/RELEASE_STRATEGY.ko.md similarity index 100% rename from RELEASE_STRATEGY.ko.md rename to docs/RELEASE_STRATEGY.ko.md diff --git a/RELEASE_STRATEGY.md b/docs/RELEASE_STRATEGY.md similarity index 100% rename from RELEASE_STRATEGY.md rename to docs/RELEASE_STRATEGY.md diff --git a/examples/pivot-model-usage.vue b/examples/pivot-model-usage.vue deleted file mode 100644 index 5833908..0000000 --- a/examples/pivot-model-usage.vue +++ /dev/null @@ -1,357 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/MemoeryTestApp.vue b/src/MemoeryTestApp.vue new file mode 100644 index 0000000..2e53ea0 --- /dev/null +++ b/src/MemoeryTestApp.vue @@ -0,0 +1,586 @@ + + + + + diff --git a/src/SImpleApp.vue b/src/SImpleApp.vue new file mode 100644 index 0000000..f132139 --- /dev/null +++ b/src/SImpleApp.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/components/pivottable-ui/VPivottableUi.vue b/src/components/pivottable-ui/VPivottableUi.vue index 5238f81..b2fcec1 100644 --- a/src/components/pivottable-ui/VPivottableUi.vue +++ b/src/components/pivottable-ui/VPivottableUi.vue @@ -137,7 +137,8 @@ import VRendererCell from './VRendererCell.vue' import VAggregatorCell from './VAggregatorCell.vue' import VDragAndDropCell from './VDragAndDropCell.vue' import VPivottable from '../pivottable/VPivottable.vue' -import { computed, watch, shallowRef, watchEffect, onUnmounted } from 'vue' +import TableRenderer from '../pivottable/renderer' +import { computed, watch, toRaw } from 'vue' import { usePropsState, useMaterializeInput, @@ -182,6 +183,7 @@ const props = withDefaults( >(), { aggregators: () => defaultAggregators, + renderers: () => TableRenderer, hiddenAttributes: () => [], hiddenFromAggregators: () => [], pivotModel: undefined, @@ -291,9 +293,10 @@ const { allFilters, materializedInput } = useMaterializeInput( ) const rendererItems = computed(() => - Object.keys(state.renderers).length ? state.renderers : {} + state.renderers && Object.keys(state.renderers).length ? state.renderers : {} ) const aggregatorItems = computed(() => state.aggregators) + const rowAttrs = computed(() => { return state.rows.filter( (e) => @@ -327,38 +330,11 @@ const unusedAttrs = computed(() => { .sort(sortAs(pivotUiState.unusedOrder)) }) -// Use shallowRef instead of computed to prevent creating new PivotData instances on every access -const pivotData = shallowRef(new PivotData(state)) - -// Update pivotData when state changes, and clean up the watcher -const stopWatcher = watchEffect(() => { - // Clean up old PivotData if exists - const oldPivotData = pivotData.value - pivotData.value = new PivotData(state) - - // Clear old data references - if (oldPivotData) { - oldPivotData.tree = {} - oldPivotData.rowKeys = [] - oldPivotData.colKeys = [] - oldPivotData.rowTotals = {} - oldPivotData.colTotals = {} - oldPivotData.filteredData = [] - } -}) - -// Clean up on unmount -onUnmounted(() => { - stopWatcher() - if (pivotData.value) { - pivotData.value.tree = {} - pivotData.value.rowKeys = [] - pivotData.value.colKeys = [] - pivotData.value.rowTotals = {} - pivotData.value.colTotals = {} - pivotData.value.filteredData = [] - } -}) +const pivotData = computed(() => new PivotData({ + ...state, + data: toRaw(state.data), + aggregators: toRaw(state.aggregators) +})) const pivotProps = computed(() => ({ data: state.data, aggregators: state.aggregators, @@ -408,22 +384,19 @@ watch( } as Partial) } }, - { deep: true, immediate: true } + { immediate: true } ) watch( - [allFilters, materializedInput], + materializedInput, () => { - // Only update the changed properties, not the entire state + // Only update data when materializedInput changes updateMultiple({ - allFilters: allFilters.value, - materializedInput: materializedInput.value, - data: materializedInput.value // Ensure data is also updated + data: materializedInput.value }) }, { - immediate: true // Add immediate to ensure initial update - // Removed deep: true - this was causing 80% of memory leak + immediate: true } ) diff --git a/src/components/pivottable/VPivottable.vue b/src/components/pivottable/VPivottable.vue index 8370858..a76c993 100644 --- a/src/components/pivottable/VPivottable.vue +++ b/src/components/pivottable/VPivottable.vue @@ -9,10 +9,27 @@ import { computed } from 'vue' import TableRenderer from './renderer' import { DefaultPropsType } from '@/types' +import { aggregators, locales } from '@/helper' -const props = defineProps() +const props = withDefaults(defineProps(), { + aggregators: () => aggregators, + aggregatorName: 'Count', + renderers: () => TableRenderer, + rendererName: 'Table', + rowOrder: 'key_a_to_z', + colOrder: 'key_a_to_z', + languagePack: () => locales, + locale: 'en', + cols: () => [], + rows: () => [], + vals: () => [], + valueFilter: () => ({}), + sorters: () => ({}), + derivedAttributes: () => ({}), + tableMaxWidth: 0 +}) const rendererComponent = computed( - () => props.renderers[props.rendererName] || TableRenderer.Table + () => (props.renderers || TableRenderer)[props.rendererName || 'Table'] || TableRenderer.Table ) diff --git a/src/components/pivottable/renderer/TSVExportRenderers.vue b/src/components/pivottable/renderer/TSVExportRenderers.vue index c6e66a4..55cbf1e 100644 --- a/src/components/pivottable/renderer/TSVExportRenderers.vue +++ b/src/components/pivottable/renderer/TSVExportRenderers.vue @@ -27,7 +27,7 @@ const headerRow = computed(() => { const header = [...pivotData.value.props.rows] if (colKeys.value.length === 1 && colKeys.value[0].length === 0) { - header.push(props.aggregatorName) + header.push(props.aggregatorName || 'Count') } else { colKeys.value.forEach((c: any[]) => header.push(c.join('-'))) } diff --git a/src/composables/index.ts b/src/composables/index.ts index 12f9052..7232b5b 100644 --- a/src/composables/index.ts +++ b/src/composables/index.ts @@ -3,5 +3,4 @@ export { useProvideFilterBox, provideFilterBox } from './useProvideFilterbox' export { useMaterializeInput } from './useMaterializeInput' export { usePropsState } from './usePropsState' export { usePivotUiState } from './usePivotUiState' -export { usePivotData } from './usePivotData' -export { usePivotModelHistory } from './usePivotModelHistory' \ No newline at end of file +export { usePivotData } from './usePivotData' \ No newline at end of file diff --git a/src/composables/useMaterializeInput.ts b/src/composables/useMaterializeInput.ts index 618f4e7..1f61076 100644 --- a/src/composables/useMaterializeInput.ts +++ b/src/composables/useMaterializeInput.ts @@ -1,4 +1,4 @@ -import { Ref, ref, watch } from 'vue' +import { Ref, ref, watch, shallowRef, ShallowRef, onUnmounted } from 'vue' import { PivotData } from '@/helper' export interface UseMaterializeInputOptions { @@ -7,8 +7,8 @@ export interface UseMaterializeInputOptions { export interface UseMaterializeInputReturn { rawData: Ref - allFilters: Ref>> - materializedInput: Ref + allFilters: ShallowRef>> + materializedInput: ShallowRef processData: (data: any) => { AllFilters: Record>; materializedInput: any[] } | void } @@ -17,8 +17,9 @@ export function useMaterializeInput ( options: UseMaterializeInputOptions ): UseMaterializeInputReturn { const rawData = ref(null) - const allFilters = ref>>({}) - const materializedInput = ref([]) + // Use shallowRef to prevent deep reactivity on large objects + const allFilters = shallowRef>>({}) + const materializedInput = shallowRef([]) function processData (data: any) { if (!data || rawData.value === data) return @@ -74,6 +75,12 @@ export function useMaterializeInput ( } ) + onUnmounted(() => { + allFilters.value = {} + materializedInput.value = [] + rawData.value = null + }) + return { rawData, allFilters, diff --git a/src/composables/usePivotModelHistory.ts b/src/composables/usePivotModelHistory.ts deleted file mode 100644 index 7fd9a90..0000000 --- a/src/composables/usePivotModelHistory.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { ref, Ref, computed, watch } from 'vue' -import { PivotModelInterface } from '@/types' -import { clonePivotModel } from '@/utils/pivotModel' - -export interface PivotModelHistoryOptions { - maxHistory?: number - autoSave?: boolean -} - -export interface UsePivotModelHistoryReturn { - history: Ref - currentIndex: Ref - canUndo: Ref - canRedo: Ref - pushState: (model: PivotModelInterface) => void - undo: () => PivotModelInterface | null - redo: () => PivotModelInterface | null - clear: () => void - getCurrentState: () => PivotModelInterface | null -} - -/** - * PivotModel์˜ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” composable - * @param modelRef PivotModel์„ ๋‹ด๊ณ  ์žˆ๋Š” ref - * @param options ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ ์˜ต์…˜ - */ -export function usePivotModelHistory( - modelRef: Ref, - options: PivotModelHistoryOptions = {} -): UsePivotModelHistoryReturn { - const { maxHistory = 50, autoSave = true } = options - - const history = ref([]) - const currentIndex = ref(-1) - - const canUndo = computed(() => currentIndex.value > 0) - const canRedo = computed(() => currentIndex.value < history.value.length - 1) - - /** - * ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ํžˆ์Šคํ† ๋ฆฌ์— ์ถ”๊ฐ€ - */ - const pushState = (model: PivotModelInterface) => { - // ํ˜„์žฌ ์ธ๋ฑ์Šค ์ดํ›„์˜ ํžˆ์Šคํ† ๋ฆฌ๋Š” ์ œ๊ฑฐ (์ƒˆ๋กœ์šด ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ) - if (currentIndex.value < history.value.length - 1) { - history.value = history.value.slice(0, currentIndex.value + 1) - } - - // ์ƒˆ ์ƒํƒœ ์ถ”๊ฐ€ - history.value.push(clonePivotModel(model)) - - // ์ตœ๋Œ€ ํžˆ์Šคํ† ๋ฆฌ ํฌ๊ธฐ ์œ ์ง€ - if (history.value.length > maxHistory) { - history.value = history.value.slice(-maxHistory) - } - - currentIndex.value = history.value.length - 1 - } - - /** - * ์ด์ „ ์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฌ๊ธฐ - */ - const undo = (): PivotModelInterface | null => { - if (!canUndo.value) return null - - currentIndex.value-- - const previousState = history.value[currentIndex.value] - - if (modelRef.value) { - Object.assign(modelRef.value, clonePivotModel(previousState)) - } - - return previousState - } - - /** - * ๋‹ค์Œ ์ƒํƒœ๋กœ ๋‹ค์‹œ ์‹คํ–‰ - */ - const redo = (): PivotModelInterface | null => { - if (!canRedo.value) return null - - currentIndex.value++ - const nextState = history.value[currentIndex.value] - - if (modelRef.value) { - Object.assign(modelRef.value, clonePivotModel(nextState)) - } - - return nextState - } - - /** - * ํžˆ์Šคํ† ๋ฆฌ ์ดˆ๊ธฐํ™” - */ - const clear = () => { - history.value = [] - currentIndex.value = -1 - } - - /** - * ํ˜„์žฌ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ - */ - const getCurrentState = (): PivotModelInterface | null => { - if (currentIndex.value >= 0 && currentIndex.value < history.value.length) { - return history.value[currentIndex.value] - } - return null - } - - // ์ž๋™ ์ €์žฅ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ชจ๋ธ ๋ณ€๊ฒฝ ๊ฐ์ง€ - if (autoSave) { - watch( - modelRef, - (newModel) => { - if (newModel) { - // ํ˜„์žฌ ํžˆ์Šคํ† ๋ฆฌ์™€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ์—๋งŒ ์ถ”๊ฐ€ - const current = getCurrentState() - if (!current || JSON.stringify(current) !== JSON.stringify(newModel)) { - pushState(newModel) - } - } - }, - { deep: true, immediate: true } - ) - } - - return { - history, - currentIndex, - canUndo, - canRedo, - pushState, - undo, - redo, - clear, - getCurrentState - } -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 98f226e..4b189e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ import { VuePivottable, VuePivottableUi } from './components' import * as PivotUtilities from './helper' import TableRenderer from './components/pivottable/renderer' import type { Component } from 'vue' -export * from './composables' const Renderer: Record = { ...TableRenderer diff --git a/src/main.ts b/src/main.ts index 98e2da2..52668a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,6 @@ import { createApp } from 'vue' - import App from './App.vue' -// import VuePivottable from '@/' const app = createApp(App) -// app.component('VuePivottableUi', VuePivottableUi) - app.mount('#app') diff --git a/src/types/index.ts b/src/types/index.ts index 35fdfa6..ef06d40 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -13,18 +13,18 @@ export interface RendererDefinition { export interface DefaultPropsType { data: any aggregators?: Record - aggregatorName: string + aggregatorName?: string heatmapMode?: 'full' | 'col' | 'row' | '' tableColorScaleGenerator?: (...args: any[]) => any tableOptions?: Record - renderers: Record - rendererName: string + renderers?: Record + rendererName?: string locale?: string languagePack?: Record showRowTotal?: boolean showColTotal?: boolean - cols: string[] - rows: string[] + cols?: string[] + rows?: string[] vals?: string[] attributes?: string[] valueFilter?: Record diff --git a/tests/pivotModel.test.ts b/tests/pivotModel.test.ts deleted file mode 100644 index add8402..0000000 --- a/tests/pivotModel.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, it, expect, vi } from 'vitest' -import { ref } from 'vue' -import { mount } from '@vue/test-utils' -import VPivottableUi from '@/components/pivottable-ui/VPivottableUi.vue' -import { PivotModelInterface } from '@/types' -import { createPivotModel, pivotModelsEqual } from '@/utils/pivotModel' -import { PivotModelSerializer } from '@/utils/pivotModelSerializer' -import { usePivotModelHistory } from '@/composables/usePivotModelHistory' - -describe('PivotModel ์–‘๋ฐฉํ–ฅ ๋ฐ”์ธ๋”ฉ', () => { - const mockData = [ - { region: 'North', quarter: 'Q1', sales: 100 }, - { region: 'North', quarter: 'Q2', sales: 150 }, - { region: 'South', quarter: 'Q1', sales: 200 }, - { region: 'South', quarter: 'Q2', sales: 250 } - ] - - it('v-model:pivotModel์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•ด์•ผ ํ•จ', async () => { - const pivotModel = ref(createPivotModel({ - rows: ['region'], - cols: ['quarter'], - vals: ['sales'], - aggregatorName: 'Sum' - })) - - const wrapper = mount(VPivottableUi, { - props: { - data: mockData, - pivotModel: pivotModel.value, - 'onUpdate:pivotModel': (e: PivotModelInterface) => { - pivotModel.value = e - } - } - }) - - // emit์ด ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ - expect(wrapper.emitted()).toBeDefined() - }) - - it('์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ emit์ด ๋ฐœ์ƒํ•ด์•ผ ํ•จ', async () => { - const wrapper = mount(VPivottableUi, { - props: { - data: mockData, - pivotModel: createPivotModel() - } - }) - - // ๋ Œ๋”๋Ÿฌ ๋ณ€๊ฒฝ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - await wrapper.vm.onUpdateRendererName('Table Heatmap') - - // emit์ด ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ํ™•์ธ - const emitted = wrapper.emitted('update:pivotModel') - expect(emitted).toBeDefined() - - if (emitted) { - const [model] = emitted[0] as [PivotModelInterface] - expect(model.rendererName).toBe('Table Heatmap') - expect(model.heatmapMode).toBe('full') - } - }) - - it('debounce๊ฐ€ ์ ์šฉ๋˜์–ด์•ผ ํ•จ', async () => { - vi.useFakeTimers() - - const wrapper = mount(VPivottableUi, { - props: { - data: mockData, - pivotModel: createPivotModel() - } - }) - - // ์—ฐ์†์ ์ธ ์ƒํƒœ ๋ณ€๊ฒฝ - await wrapper.vm.onUpdateValueFilter({ key: 'region', value: ['North'] }) - await wrapper.vm.onUpdateValueFilter({ key: 'region', value: ['North', 'South'] }) - - // debounce ์ „์—๋Š” emit์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Œ - expect(wrapper.emitted('update:pivotModel')).toBeUndefined() - - // 100ms ํ›„ emit ๋ฐœ์ƒ - vi.advanceTimersByTime(100) - expect(wrapper.emitted('update:pivotModel')).toBeDefined() - - vi.useRealTimers() - }) -}) - -describe('PivotModel ์œ ํ‹ธ๋ฆฌํ‹ฐ', () => { - it('๋‘ ๋ชจ๋ธ์ด ๋™์ผํ•œ์ง€ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ', () => { - const model1 = createPivotModel({ - rows: ['region'], - cols: ['quarter'], - vals: ['sales'] - }) - - const model2 = createPivotModel({ - rows: ['region'], - cols: ['quarter'], - vals: ['sales'] - }) - - const model3 = createPivotModel({ - rows: ['region'], - cols: ['quarter'], - vals: ['amount'] // ๋‹ค๋ฅธ ๊ฐ’ - }) - - expect(pivotModelsEqual(model1, model2)).toBe(true) - expect(pivotModelsEqual(model1, model3)).toBe(false) - }) -}) - -describe('PivotModelSerializer', () => { - const testModel = createPivotModel({ - rows: ['region'], - cols: ['quarter'], - vals: ['sales'], - aggregatorName: 'Average', - valueFilter: { region: ['North'] } - }) - - it('JSON ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๊ฐ€ ์ž‘๋™ํ•ด์•ผ ํ•จ', () => { - const json = PivotModelSerializer.serialize(testModel) - const deserialized = PivotModelSerializer.deserialize(json) - - expect(deserialized.rows).toEqual(testModel.rows) - expect(deserialized.cols).toEqual(testModel.cols) - expect(deserialized.vals).toEqual(testModel.vals) - expect(deserialized.aggregatorName).toBe(testModel.aggregatorName) - expect(deserialized.valueFilter).toEqual(testModel.valueFilter) - }) - - it('URL ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€ํ™˜์ด ์ž‘๋™ํ•ด์•ผ ํ•จ', () => { - const params = PivotModelSerializer.toUrlParams(testModel) - - expect(params.get('rows')).toBe('region') - expect(params.get('cols')).toBe('quarter') - expect(params.get('vals')).toBe('sales') - expect(params.get('aggregatorName')).toBe('Average') - expect(params.get('valueFilter')).toBe(JSON.stringify({ region: ['North'] })) - - const restored = PivotModelSerializer.fromUrlParams(params) - expect(restored.rows).toEqual(['region']) - expect(restored.cols).toEqual(['quarter']) - expect(restored.valueFilter).toEqual({ region: ['North'] }) - }) - - it('Base64 ์ธ์ฝ”๋”ฉ/๋””์ฝ”๋”ฉ์ด ์ž‘๋™ํ•ด์•ผ ํ•จ', () => { - const base64 = PivotModelSerializer.toBase64(testModel) - const decoded = PivotModelSerializer.fromBase64(base64) - - expect(decoded.rows).toEqual(testModel.rows) - expect(decoded.cols).toEqual(testModel.cols) - expect(decoded.aggregatorName).toBe(testModel.aggregatorName) - }) -}) - -describe('usePivotModelHistory', () => { - it('์ƒํƒœ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ', () => { - const model = ref(createPivotModel({ - rows: ['region'], - cols: ['quarter'] - })) - - const { - history, - currentIndex, - canUndo, - canRedo, - pushState, - undo, - redo - } = usePivotModelHistory(model, { autoSave: false }) - - // ์ดˆ๊ธฐ ์ƒํƒœ - expect(history.value.length).toBe(0) - expect(canUndo.value).toBe(false) - expect(canRedo.value).toBe(false) - - // ์ƒํƒœ ์ถ”๊ฐ€ - pushState(model.value) - expect(history.value.length).toBe(1) - expect(currentIndex.value).toBe(0) - - // ์ƒˆ๋กœ์šด ์ƒํƒœ - model.value.rows = ['region', 'product'] - pushState(model.value) - expect(history.value.length).toBe(2) - expect(canUndo.value).toBe(true) - - // ์‹คํ–‰ ์ทจ์†Œ - const undoState = undo() - expect(undoState).toBeDefined() - expect(undoState?.rows).toEqual(['region']) - expect(canRedo.value).toBe(true) - - // ๋‹ค์‹œ ์‹คํ–‰ - const redoState = redo() - expect(redoState).toBeDefined() - expect(redoState?.rows).toEqual(['region', 'product']) - }) - - it('์ž๋™ ์ €์žฅ์ด ์ž‘๋™ํ•ด์•ผ ํ•จ', async () => { - const model = ref(createPivotModel({ - rows: ['region'] - })) - - const { history } = usePivotModelHistory(model, { autoSave: true }) - - // ์ดˆ๊ธฐ ์ƒํƒœ๊ฐ€ ์ž๋™ ์ €์žฅ๋จ - await new Promise(resolve => setTimeout(resolve, 0)) - expect(history.value.length).toBe(1) - - // ๋ชจ๋ธ ๋ณ€๊ฒฝ - model.value.rows = ['region', 'product'] - await new Promise(resolve => setTimeout(resolve, 0)) - expect(history.value.length).toBe(2) - }) -}) \ No newline at end of file