Skip to content

Commit 944b73a

Browse files
committed
feat: implement PivotModel two-way binding with complete features
- Add PivotModelInterface type definition - Implement v-model:pivotModel two-way binding - Add emit events for state changes with debounce optimization - Add state history management (usePivotModelHistory) - Add serialization/deserialization utilities - Add props to state synchronization - Update App.vue to demonstrate PivotModel binding - Add comprehensive test cases and usage examples
1 parent 2e6fcd6 commit 944b73a

File tree

14 files changed

+2035
-21
lines changed

14 files changed

+2035
-21
lines changed

PIVOT_MODEL_FEATURE_DEVELOPMENT.md

Lines changed: 841 additions & 0 deletions
Large diffs are not rendered by default.

examples/pivot-model-usage.vue

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
<template>
2+
<div class="pivot-model-example">
3+
<h1>PivotModel 양방향 바인딩 사용 예제</h1>
4+
5+
<!-- 기본 사용법 -->
6+
<section class="basic-usage">
7+
<h2>기본 사용법</h2>
8+
<div class="controls">
9+
<button @click="resetModel">모델 초기화</button>
10+
<button @click="saveToLocalStorage">로컬 저장</button>
11+
<button @click="loadFromLocalStorage">로컬 로드</button>
12+
</div>
13+
14+
<div class="model-status">
15+
<h3>현재 PivotModel 상태:</h3>
16+
<pre>{{ JSON.stringify(pivotModel, null, 2) }}</pre>
17+
</div>
18+
19+
<VPivottableUi
20+
v-model:pivot-model="pivotModel"
21+
:data="salesData"
22+
@change="onPivotChange"
23+
/>
24+
</section>
25+
26+
<!-- 히스토리 관리 예제 -->
27+
<section class="history-example">
28+
<h2>상태 히스토리 관리</h2>
29+
<div class="history-controls">
30+
<button
31+
@click="undo"
32+
:disabled="!canUndo"
33+
>
34+
← 실행 취소
35+
</button>
36+
<span>{{ currentIndex + 1 }} / {{ history.length }}</span>
37+
<button
38+
@click="redo"
39+
:disabled="!canRedo"
40+
>
41+
다시 실행 →
42+
</button>
43+
</div>
44+
45+
<VPivottableUi
46+
v-model:pivot-model="historyModel"
47+
:data="salesData"
48+
/>
49+
</section>
50+
51+
<!-- URL 동기화 예제 -->
52+
<section class="url-sync-example">
53+
<h2>URL 파라미터 동기화</h2>
54+
<div class="url-controls">
55+
<button @click="syncToUrl">URL에 저장</button>
56+
<button @click="loadFromUrl">URL에서 로드</button>
57+
<button @click="shareUrl">공유 링크 생성</button>
58+
</div>
59+
60+
<div
61+
v-if="shareLink"
62+
class="share-link"
63+
>
64+
<p>공유 링크:</p>
65+
<code>{{ shareLink }}</code>
66+
</div>
67+
68+
<VPivottableUi
69+
v-model:pivot-model="urlModel"
70+
:data="salesData"
71+
/>
72+
</section>
73+
74+
<!-- 다중 피벗테이블 동기화 -->
75+
<section class="sync-example">
76+
<h2>다중 피벗테이블 동기화</h2>
77+
<div class="sync-controls">
78+
<label>
79+
<input
80+
type="checkbox"
81+
v-model="syncEnabled"
82+
/>
83+
동기화 활성화
84+
</label>
85+
</div>
86+
87+
<div class="pivot-grid">
88+
<div class="pivot-item">
89+
<h3>매출 분석</h3>
90+
<VPivottableUi
91+
v-model:pivot-model="masterModel"
92+
:data="salesData"
93+
@change="onMasterChange"
94+
/>
95+
</div>
96+
97+
<div class="pivot-item">
98+
<h3>고객 분석 (동기화됨)</h3>
99+
<VPivottableUi
100+
v-model:pivot-model="slaveModel"
101+
:data="customerData"
102+
/>
103+
</div>
104+
</div>
105+
</section>
106+
</div>
107+
</template>
108+
109+
<script setup lang="ts">
110+
import { ref, onMounted } from 'vue'
111+
import { VPivottableUi } from '@/components'
112+
import { PivotModelInterface } from '@/types'
113+
import { createPivotModel } from '@/utils/pivotModel'
114+
import { PivotModelSerializer } from '@/utils/pivotModelSerializer'
115+
import { usePivotModelHistory } from '@/composables/usePivotModelHistory'
116+
117+
// 샘플 데이터
118+
const salesData = ref([
119+
{ region: 'North', quarter: 'Q1', product: 'A', sales: 100, quantity: 10 },
120+
{ region: 'North', quarter: 'Q1', product: 'B', sales: 150, quantity: 15 },
121+
{ region: 'North', quarter: 'Q2', product: 'A', sales: 120, quantity: 12 },
122+
{ region: 'North', quarter: 'Q2', product: 'B', sales: 180, quantity: 18 },
123+
{ region: 'South', quarter: 'Q1', product: 'A', sales: 200, quantity: 20 },
124+
{ region: 'South', quarter: 'Q1', product: 'B', sales: 250, quantity: 25 },
125+
{ region: 'South', quarter: 'Q2', product: 'A', sales: 220, quantity: 22 },
126+
{ region: 'South', quarter: 'Q2', product: 'B', sales: 280, quantity: 28 },
127+
{ region: 'East', quarter: 'Q1', product: 'A', sales: 150, quantity: 15 },
128+
{ region: 'East', quarter: 'Q1', product: 'B', sales: 200, quantity: 20 },
129+
{ region: 'East', quarter: 'Q2', product: 'A', sales: 170, quantity: 17 },
130+
{ region: 'East', quarter: 'Q2', product: 'B', sales: 230, quantity: 23 }
131+
])
132+
133+
const customerData = ref([
134+
{ region: 'North', quarter: 'Q1', segment: 'Enterprise', customers: 50 },
135+
{ region: 'North', quarter: 'Q1', segment: 'SMB', customers: 100 },
136+
{ region: 'North', quarter: 'Q2', segment: 'Enterprise', customers: 55 },
137+
{ region: 'North', quarter: 'Q2', segment: 'SMB', customers: 110 },
138+
{ region: 'South', quarter: 'Q1', segment: 'Enterprise', customers: 40 },
139+
{ region: 'South', quarter: 'Q1', segment: 'SMB', customers: 80 },
140+
{ region: 'South', quarter: 'Q2', segment: 'Enterprise', customers: 45 },
141+
{ region: 'South', quarter: 'Q2', segment: 'SMB', customers: 90 }
142+
])
143+
144+
// 1. 기본 사용법
145+
const pivotModel = ref<PivotModelInterface>(createPivotModel({
146+
rows: ['region'],
147+
cols: ['quarter'],
148+
vals: ['sales'],
149+
aggregatorName: 'Sum',
150+
rendererName: 'Table'
151+
}))
152+
153+
const onPivotChange = (newModel: PivotModelInterface) => {
154+
console.log('피벗 모델 변경:', newModel)
155+
// 여기서 서버로 상태를 저장하거나 다른 작업을 수행할 수 있습니다
156+
}
157+
158+
const resetModel = () => {
159+
pivotModel.value = createPivotModel()
160+
}
161+
162+
const saveToLocalStorage = () => {
163+
PivotModelSerializer.saveToLocalStorage('myPivotModel', pivotModel.value)
164+
alert('로컬 스토리지에 저장되었습니다!')
165+
}
166+
167+
const loadFromLocalStorage = () => {
168+
const loaded = PivotModelSerializer.loadFromLocalStorage('myPivotModel')
169+
if (loaded) {
170+
pivotModel.value = loaded
171+
alert('로컬 스토리지에서 로드되었습니다!')
172+
} else {
173+
alert('저장된 모델이 없습니다.')
174+
}
175+
}
176+
177+
// 2. 히스토리 관리
178+
const historyModel = ref<PivotModelInterface>(createPivotModel({
179+
rows: ['product'],
180+
cols: ['region'],
181+
vals: ['quantity'],
182+
aggregatorName: 'Average'
183+
}))
184+
185+
const {
186+
history,
187+
currentIndex,
188+
canUndo,
189+
canRedo,
190+
undo,
191+
redo
192+
} = usePivotModelHistory(historyModel)
193+
194+
// 3. URL 동기화
195+
const urlModel = ref<PivotModelInterface>(createPivotModel())
196+
const shareLink = ref<string>('')
197+
198+
const syncToUrl = () => {
199+
const params = PivotModelSerializer.toUrlParams(urlModel.value)
200+
const newUrl = `${window.location.pathname}?${params.toString()}`
201+
window.history.pushState({}, '', newUrl)
202+
alert('URL에 저장되었습니다!')
203+
}
204+
205+
const loadFromUrl = () => {
206+
const params = new URLSearchParams(window.location.search)
207+
const loaded = PivotModelSerializer.fromUrlParams(params)
208+
if (Object.keys(loaded).length > 0) {
209+
urlModel.value = createPivotModel(loaded)
210+
alert('URL에서 로드되었습니다!')
211+
} else {
212+
alert('URL에 저장된 모델이 없습니다.')
213+
}
214+
}
215+
216+
const shareUrl = () => {
217+
const params = PivotModelSerializer.toUrlParams(urlModel.value)
218+
shareLink.value = `${window.location.origin}${window.location.pathname}?${params.toString()}`
219+
}
220+
221+
// 4. 다중 피벗테이블 동기화
222+
const syncEnabled = ref(false)
223+
const masterModel = ref<PivotModelInterface>(createPivotModel({
224+
rows: ['region'],
225+
cols: ['quarter'],
226+
vals: ['sales']
227+
}))
228+
const slaveModel = ref<PivotModelInterface>(createPivotModel({
229+
rows: ['region'],
230+
cols: ['quarter'],
231+
vals: ['customers']
232+
}))
233+
234+
const onMasterChange = (newModel: PivotModelInterface) => {
235+
if (syncEnabled.value) {
236+
// 구조만 동기화하고 vals는 유지
237+
slaveModel.value = {
238+
...slaveModel.value,
239+
rows: newModel.rows,
240+
cols: newModel.cols,
241+
aggregatorName: newModel.aggregatorName,
242+
rowOrder: newModel.rowOrder,
243+
colOrder: newModel.colOrder,
244+
valueFilter: newModel.valueFilter
245+
}
246+
}
247+
}
248+
249+
// 페이지 로드 시 URL에서 상태 복원
250+
onMounted(() => {
251+
loadFromUrl()
252+
})
253+
</script>
254+
255+
<style scoped>
256+
.pivot-model-example {
257+
max-width: 1400px;
258+
margin: 0 auto;
259+
padding: 20px;
260+
}
261+
262+
section {
263+
margin-bottom: 60px;
264+
padding: 20px;
265+
border: 1px solid #e0e0e0;
266+
border-radius: 8px;
267+
}
268+
269+
h1, h2, h3 {
270+
margin-bottom: 20px;
271+
}
272+
273+
.controls, .history-controls, .url-controls, .sync-controls {
274+
margin-bottom: 20px;
275+
display: flex;
276+
gap: 10px;
277+
align-items: center;
278+
}
279+
280+
button {
281+
padding: 8px 16px;
282+
border: 1px solid #ddd;
283+
border-radius: 4px;
284+
background: white;
285+
cursor: pointer;
286+
transition: all 0.3s;
287+
}
288+
289+
button:hover:not(:disabled) {
290+
background: #f0f0f0;
291+
}
292+
293+
button:disabled {
294+
opacity: 0.5;
295+
cursor: not-allowed;
296+
}
297+
298+
.model-status {
299+
margin-bottom: 20px;
300+
padding: 15px;
301+
background: #f5f5f5;
302+
border-radius: 4px;
303+
}
304+
305+
.model-status pre {
306+
margin: 0;
307+
font-size: 12px;
308+
max-height: 200px;
309+
overflow-y: auto;
310+
}
311+
312+
.share-link {
313+
margin-bottom: 20px;
314+
padding: 15px;
315+
background: #e8f5e9;
316+
border-radius: 4px;
317+
}
318+
319+
.share-link code {
320+
display: block;
321+
padding: 10px;
322+
background: white;
323+
border-radius: 4px;
324+
word-break: break-all;
325+
font-size: 12px;
326+
}
327+
328+
.pivot-grid {
329+
display: grid;
330+
grid-template-columns: 1fr 1fr;
331+
gap: 20px;
332+
}
333+
334+
.pivot-item {
335+
border: 1px solid #e0e0e0;
336+
padding: 15px;
337+
border-radius: 4px;
338+
}
339+
340+
.pivot-item h3 {
341+
margin-bottom: 15px;
342+
color: #333;
343+
}
344+
345+
label {
346+
display: flex;
347+
align-items: center;
348+
gap: 8px;
349+
cursor: pointer;
350+
}
351+
352+
input[type="checkbox"] {
353+
width: 18px;
354+
height: 18px;
355+
cursor: pointer;
356+
}
357+
</style>

0 commit comments

Comments
 (0)