Skip to content

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)

알려진 제한사항

  1. 터치 인터페이스: 복잡한 멀티터치 제스처는 제한적 지원
  2. 성능: 고해상도 캔버스에서 연속적인 스케일 변경 시 성능 저하 가능
  3. 브라우저 호환성: 일부 오래된 브라우저에서 터치 이벤트 지원 제한

활용 사례

  1. 토폴로지 차트: 네트워크 다이어그램 확대/축소
  2. 이미지 뷰어: 고해상도 이미지 상세 보기
  3. 캔버스 에디터: 그래픽 편집 도구의 줌 기능
  4. 데이터 시각화: 복잡한 차트의 상세 분석
  5. 지도 애플리케이션: 지도 영역 확대/축소
  6. 게임 인터페이스: 미니맵이나 전략 뷰어