Appearance
ScaleController
차트나 캔버스 요소의 확대/축소를 제어하는 UI 컨트롤러 컴포넌트입니다. 마우스 휠, 터치 제스처, 버튼 클릭을 통한 스케일 조정을 지원합니다.
개요
ScaleController는 토폴로지 차트, 이미지 뷰어, 캔버스 기반 애플리케이션 등에서 사용할 수 있는 범용 스케일 컨트롤러입니다. 직관적인 UI와 다양한 입력 방식을 통해 사용자가 콘텐츠의 확대/축소를 쉽게 조절할 수 있습니다.
주요 특징
- 🎯 다양한 입력 지원: 마우스 휠, 터치 핀치, 버튼 클릭
- 📱 터치 최적화: 모바일 디바이스의 핀치 제스처 지원
- 🎨 위치 커스터마이징: 4가지 코너 위치 선택 가능
- ⚙️ 완전 커스터마이징: 크기, 마진, 아이콘 등 자유로운 스타일링
- ♿ 접근성: 키보드 네비게이션과 스크린 리더 지원
- 🔧 이벤트 기반: 유연한 스케일 로직 구현 가능
Props Interface
typescript
interface ScaleControllerProps {
/** 컨트롤러 위치 (기본값: 'bottom-right') */
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
/** 컨트롤러 표시 여부 (기본값: true) */
visible?: boolean;
/** 마우스 휠 이벤트 활성화 여부 (기본값: true) */
enableWheel?: boolean;
/** 터치 이벤트 활성화 여부 (기본값: true) */
enableTouch?: boolean;
/** 버튼 크기 (기본값: 32px) */
buttonSize?: number;
/** 컨트롤러 마진 (기본값: 8px) */
margin?: number;
}
Events
typescript
interface ScaleControllerEmits {
/** 마우스 휠 이벤트 (확대/축소) */
'wheel': [event: WheelEvent];
/** '+' 버튼 클릭 (확대) */
'clickUp': [];
/** '-' 버튼 클릭 (축소) */
'clickDown': [];
/** '원복' 버튼 클릭 (초기 스케일로 복원) */
'clickReset': [];
}
기본 사용법
vue
<template>
<div class="chart-container">
<ScaleController
position="bottom-right"
:visible="true"
@wheel="handleWheel"
@click-up="handleZoomIn"
@click-down="handleZoomOut"
@click-reset="handleReset"
>
<!-- 스케일 대상 콘텐츠 -->
<canvas
ref="chartCanvas"
:width="canvasWidth"
:height="canvasHeight"
:style="{
transform: `scale(${scale})`,
transformOrigin: '0 0'
}"
/>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ScaleController } from '@jennifersoft/apm-components';
const scale = ref<number>(1);
const minScale = 0.1;
const maxScale = 5;
const scaleStep = 0.1;
const handleWheel = (event: WheelEvent) => {
const delta = event.deltaY > 0 ? -scaleStep : scaleStep;
updateScale(scale.value + delta);
};
const handleZoomIn = () => {
updateScale(scale.value + scaleStep);
};
const handleZoomOut = () => {
updateScale(scale.value - scaleStep);
};
const handleReset = () => {
updateScale(1);
};
const updateScale = (newScale: number) => {
scale.value = Math.max(minScale, Math.min(maxScale, newScale));
};
</script>
SVG 차트와 함께 사용
vue
<template>
<div class="svg-chart-container">
<ScaleController
position="top-right"
:button-size="28"
:margin="12"
@wheel="handleWheel"
@click-up="zoomIn"
@click-down="zoomOut"
@click-reset="resetZoom"
>
<svg
ref="svgChart"
:width="chartWidth"
:height="chartHeight"
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
>
<!-- SVG 차트 내용 -->
<g v-for="node in nodes" :key="node.id">
<circle
:cx="node.x"
:cy="node.y"
:r="node.radius"
:fill="node.color"
/>
<text
:x="node.x"
:y="node.y"
text-anchor="middle"
dominant-baseline="middle"
>
{{ node.label }}
</text>
</g>
<g v-for="edge in edges" :key="edge.id">
<line
:x1="edge.source.x"
:y1="edge.source.y"
:x2="edge.target.x"
:y2="edge.target.y"
stroke="#999"
stroke-width="2"
/>
</g>
</svg>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
const chartWidth = 800;
const chartHeight = 600;
const viewBox = reactive({
x: 0,
y: 0,
width: chartWidth,
height: chartHeight
});
const baseViewBox = { ...viewBox };
const zoomLevel = ref<number>(1);
const handleWheel = (event: WheelEvent) => {
const zoomFactor = event.deltaY > 0 ? 1.1 : 0.9;
zoom(zoomFactor, event.offsetX, event.offsetY);
};
const zoomIn = () => {
zoom(0.9, chartWidth / 2, chartHeight / 2);
};
const zoomOut = () => {
zoom(1.1, chartWidth / 2, chartHeight / 2);
};
const resetZoom = () => {
Object.assign(viewBox, baseViewBox);
zoomLevel.value = 1;
};
const zoom = (factor: number, centerX: number, centerY: number) => {
const newWidth = viewBox.width * factor;
const newHeight = viewBox.height * factor;
// 줌 제한
if (newWidth > chartWidth * 10 || newWidth < chartWidth * 0.1) return;
const dx = (newWidth - viewBox.width) * (centerX / chartWidth);
const dy = (newHeight - viewBox.height) * (centerY / chartHeight);
viewBox.x -= dx;
viewBox.y -= dy;
viewBox.width = newWidth;
viewBox.height = newHeight;
zoomLevel.value = chartWidth / viewBox.width;
};
</script>
토폴로지 차트와 통합
vue
<template>
<div class="topology-with-scale">
<ScaleController
position="bottom-left"
:enable-wheel="true"
:enable-touch="true"
@wheel="handleTopologyWheel"
@click-up="handleTopologyZoomIn"
@click-down="handleTopologyZoomOut"
@click-reset="handleTopologyReset"
>
<TopologyChart
ref="topologyRef"
:data="topologyData"
:width="800"
:height="600"
:enable-auto-scale="false"
/>
</ScaleController>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ScaleController } from '@jennifersoft/apm-components';
import { TopologyChart } from '@jennifersoft/apm-components';
const topologyRef = ref<InstanceType<typeof TopologyChart>>();
const handleTopologyWheel = (event: WheelEvent) => {
if (!topologyRef.value) return;
// TopologyChart의 내장 줌 기능 사용
const zoomEvent = new WheelEvent('wheel', {
deltaY: event.deltaY,
clientX: event.clientX,
clientY: event.clientY,
bubbles: true,
cancelable: true
});
topologyRef.value.$el.dispatchEvent(zoomEvent);
};
const handleTopologyZoomIn = () => {
// 프로그래밍 방식으로 줌 인
const wheelEvent = new WheelEvent('wheel', {
deltaY: -100,
bubbles: true,
cancelable: true
});
topologyRef.value?.$el.dispatchEvent(wheelEvent);
};
const handleTopologyZoomOut = () => {
// 프로그래밍 방식으로 줌 아웃
const wheelEvent = new WheelEvent('wheel', {
deltaY: 100,
bubbles: true,
cancelable: true
});
topologyRef.value?.$el.dispatchEvent(wheelEvent);
};
const handleTopologyReset = () => {
// 토폴로지 차트 리셋 (차트의 내장 메서드 사용)
topologyRef.value?.resetZoom();
};
</script>
터치 제스처 처리
vue
<template>
<ScaleController
:enable-touch="true"
@wheel="handlePinchZoom"
>
<div
class="touch-area"
:style="{
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
transformOrigin: '0 0'
}"
>
<!-- 터치 대상 콘텐츠 -->
<img src="/large-image.jpg" alt="확대/축소 가능한 이미지" />
</div>
</ScaleController>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const scale = ref<number>(1);
const translateX = ref<number>(0);
const translateY = ref<number>(0);
const handlePinchZoom = (event: WheelEvent) => {
// 터치 핀치는 WheelEvent로 변환되어 전달됨
const scaleFactor = event.deltaY > 0 ? 0.95 : 1.05;
const newScale = scale.value * scaleFactor;
// 스케일 제한
if (newScale >= 0.5 && newScale <= 3) {
scale.value = newScale;
}
};
</script>
커스텀 스타일링
vue
<template>
<ScaleController
position="top-left"
:button-size="40"
:margin="20"
class="custom-scale-controller"
>
<slot />
</ScaleController>
</template>
<style scoped>
.custom-scale-controller {
/* 커스텀 CSS 변수 오버라이드 */
--button-size: 40px;
--margin: 20px;
}
.custom-scale-controller :deep(.scale-controls__btn) {
/* 버튼 스타일 커스터마이징 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.custom-scale-controller :deep(.scale-controls__btn:hover) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.custom-scale-controller :deep(.scale-controls__btn:active) {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* 다크 테마 지원 */
@media (prefers-color-scheme: dark) {
.custom-scale-controller :deep(.scale-controls__btn) {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
border: 1px solid #4a5568;
}
}
</style>
구성 요소
ScaleController는 다음 구성 요소들로 이루어져 있습니다:
버튼 종류
- 확대 버튼 (
+
): 스케일 증가 - 원복 버튼 (⛶): 초기 스케일로 복원
- 축소 버튼 (
-
): 스케일 감소
이벤트 처리
- 마우스 휠: 부드러운 확대/축소
- 터치 핀치: 모바일 디바이스 지원
- 버튼 클릭: 정확한 스케일 조정
설정 옵션
- 위치: 4개 코너 중 선택
- 크기: 버튼 크기와 마진 조정
- 활성화: 개별 이벤트 타입 제어
의존성
@jennifersoft/vue-components-v2
: SvgIcon, ICON_TYPE- Vue 3: Composition API 기반
- 터치 이벤트 지원을 위한 모던 브라우저
브라우저 지원
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
- 모바일 브라우저 (iOS Safari, Android Chrome)
알려진 제한사항
- 터치 인터페이스: 복잡한 멀티터치 제스처는 제한적 지원
- 성능: 고해상도 캔버스에서 연속적인 스케일 변경 시 성능 저하 가능
- 브라우저 호환성: 일부 오래된 브라우저에서 터치 이벤트 지원 제한
활용 사례
- 토폴로지 차트: 네트워크 다이어그램 확대/축소
- 이미지 뷰어: 고해상도 이미지 상세 보기
- 캔버스 에디터: 그래픽 편집 도구의 줌 기능
- 데이터 시각화: 복잡한 차트의 상세 분석
- 지도 애플리케이션: 지도 영역 확대/축소
- 게임 인터페이스: 미니맵이나 전략 뷰어