Skip to content

NavigationBar

좌측 수직 네비게이션 사이드바 컴포넌트

개요

NavigationBar는 APM 애플리케이션의 메인 네비게이션 인터페이스를 제공하는 좌측 수직 사이드바 컴포넌트입니다. 메뉴 클릭 시 슬라이드 패널이 열리며, 대시보드, 분석, 통계 등의 주요 기능에 접근할 수 있습니다.

주요 기능

  • 📱 수직 사이드바: 72px 고정 너비의 좌측 네비게이션 바
  • 🎯 슬라이드 패널: 메뉴 클릭 시 256px (기본값) 너비의 패널이 슬라이드로 열림
  • 🔄 토글 동작: 같은 메뉴 재클릭 시 패널 닫기, 다른 메뉴 클릭 시 패널 전환
  • 🌐 Teleport 지원: Portal을 통한 유연한 DOM 배치
  • 🏷️ 배지 표시: 알림 개수 등을 표시하는 배지 지원
  • 🎨 커스터마이징: 로고, 메뉴 아이템, 패널 콘텐츠 완전 커스터마이징
  • 🔌 독립 패널: 일반 패널과 별도로 작동하는 커스텀 패널 지원
  • 접근성: 외부 클릭 감지, 키보드 네비게이션

Props

속성타입기본값설명
headMenuItemsMenuItem[][]상단 메뉴 아이템 목록
tailMenuItemsMenuItem[][]하단 메뉴 아이템 목록
logoSrcstring-로고 이미지 소스 (없으면 #logo 슬롯 사용)
logoHrefstring'/'로고 클릭 시 이동할 URL
selectedPanelstring | nullnull현재 선택된 패널 키 (v-model)
panelWidthnumber256패널 너비(px) - 일반 패널과 커스텀 패널 모두에 적용
teleportTostring | null'body'Teleport 대상 선택자
enableCustomPanelbooleanfalse커스텀 패널 활성화 여부
customPanelKeystring'custom'커스텀 패널 식별 키
classstring-추가 CSS 클래스
typescript
interface MenuItem {
  /** 메뉴 식별자 */
  key: string;
  /** 아이콘 (ICON_TYPE) */
  icon: IconTypes;
  /** 표시 이름 */
  displayName: string;
  /** 메뉴 타입 */
  type: 'panel' | 'event';  // panel: 패널 열림, event: 이벤트만 발생
  /** 선택 상태 */
  selected?: boolean;
  /** 눌림 상태 (패널이 열려있을 때 시각적 피드백) */
  pressed?: boolean;
  /** 비활성화 상태 */
  disabled?: boolean;
  /** 배지 (알림 개수 등) */
  badge?: number | string;
  /** 툴팁 텍스트 */
  tooltip?: string;
}
typescript
interface SubMenuItem {
  /** 메뉴 식별자 */
  key: string;
  /** 아이콘 (thumbnail이 있으면 선택사항) */
  icon?: IconTypes;
  /** 표시 이름 */
  displayName: string;
  /** 선택 상태 */
  selected?: boolean;
  /** 눌림 상태 (클릭 시 임시 시각적 피드백) */
  pressed?: boolean;
  /** 비활성화 상태 */
  disabled?: boolean;
  /** 검색 하이라이트 인덱스 */
  indices?: number[];
  /** 썸네일 이미지 경로 (아이콘 대신 사용) */
  thumbnail?: string;
  /** 툴팁 텍스트 */
  tooltip?: string;
}

Events

이벤트파라미터설명
menu-clickitem: MenuItem메뉴 클릭 시 발생
panel-openpanelKey: string, item?: MenuItem패널 열림 시 발생
panel-closepanelKey: string, item?: MenuItem패널 닫힘 시 발생
logo-click-로고 클릭 시 발생
update:selectedPanelvalue: string | nullv-model 업데이트

Slots

로고 영역 슬롯. logoSrc prop이 없을 때 사용됩니다.

vue
<NavigationBar>
  <template #logo>
    <div class="custom-logo">MY APP</div>
  </template>
</NavigationBar>

#panel-

각 패널의 전체 콘텐츠를 정의하는 슬롯. {key}MenuItem.key 값입니다.

vue
<NavigationBar>
  <template #panel-dashboard>
    <NavHeader title="대시보드" closeable @close="closePanel" />
    <div class="panel-content">
      <!-- 패널 콘텐츠 -->
    </div>
  </template>
</NavigationBar>

#custom-panel

독립적인 커스텀 패널 슬롯. enableCustomPanel이 true일 때 표시됩니다.

vue
<NavigationBar :enable-custom-panel="showCustom">
  <template #custom-panel>
    <NavHeader title="커스텀" closeable @close="closeCustomPanel" />
    <div class="panel-content">
      <!-- 커스텀 패널 콘텐츠 -->
    </div>
  </template>
</NavigationBar>

TypeScript 타입

typescript
import type {
  MenuItem,
  SubMenuItem,
  NavigationBarProps,
  NavigationBarEmits
} from '@jennifersoft/apm-components';

관련 컴포넌트

NavigationBar와 함께 사용되는 주요 컴포넌트들입니다.

패널 상단에 표시되는 헤더 컴포넌트입니다.

Props

속성타입기본값설명
titlestring-헤더 타이틀 텍스트
tailIconIconTypes-우측 커스텀 아이콘 (closeable이 false일 때)
closeablebooleanfalse닫기 아이콘 표시 및 close 이벤트 발생 여부
clickablebooleanfalse헤더 클릭 가능 여부

Events

이벤트파라미터설명
clickevent: MouseEvent헤더 클릭 시 발생 (clickable이 true일 때)
close-닫기 아이콘 클릭 시 발생 (closeable이 true일 때)

사용 예시

vue
<NavHeader
  title="대시보드"
  closeable
  @close="closePanel"
/>

패널 내부의 서브메뉴 아이템 컴포넌트입니다.

Props

속성타입기본값설명
iconIconTypes-아이콘 (thumbnail이 없을 때 필수)
namestring-표시 이름
selectedbooleanfalse선택 상태 (보라색 하이라이트)
pressedbooleanfalse눌림 상태 (회색 오버레이)
disabledbooleanfalse비활성화 상태
indicesnumber[]-검색어 하이라이트 문자 인덱스 배열
thumbnailstring-썸네일 이미지 URL (67×48px, 아이콘 대신 표시)
tooltipstring-툴팁 텍스트

Events

이벤트파라미터설명
clickevent: MouseEvent아이템 클릭 시 발생

사용 예시

vue
<SubNavItem
  v-for="item in dashboardItems"
  :key="item.key"
  :icon="item.icon"
  :name="item.displayName"
  :selected="item.selected"
  @click="navigateTo(item.key)"
/>

SubHeader

패널 내부의 섹션 구분 헤더입니다.

Props

속성타입기본값설명
titlestring-섹션 타이틀

사용 예시

vue
<SubHeader title="최근 대시보드" />
<SubNavItem v-for="item in recentItems" ... />

기본 사용법

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  NavigationBar,
  NavHeader,
  SubNavItem,
  type MenuItem
} from '@jennifersoft/apm-components';
import { ICON_TYPE } from '@jennifersoft/vue-components-v2';

const selectedPanel = ref<string | null>(null);

const headMenuItems = ref<MenuItem[]>([
  {
    key: 'dashboard',
    icon: ICON_TYPE.dashboard,
    displayName: '대시보드',
    type: 'panel'
  },
  {
    key: 'analysis',
    icon: ICON_TYPE.analysis,
    displayName: '분석',
    type: 'panel'
  }
]);

const tailMenuItems = ref<MenuItem[]>([
  {
    key: 'settings',
    icon: ICON_TYPE.settings,
    displayName: '설정',
    type: 'panel'
  },
  {
    key: 'user',
    icon: ICON_TYPE.userStroke,
    displayName: '사용자',
    type: 'panel'
  }
]);

const closePanel = () => {
  selectedPanel.value = null;
};
</script>

<template>
  <NavigationBar
    v-model:selected-panel="selectedPanel"
    :head-menu-items="headMenuItems"
    :tail-menu-items="tailMenuItems"
    logo-src="/assets/logo.png"
  >
    <!-- Dashboard Panel -->
    <template #panel-dashboard>
      <NavHeader title="대시보드" closeable @close="closePanel" />
      <div class="panel-content">
        <p>대시보드 콘텐츠</p>
      </div>
    </template>

    <!-- Analysis Panel -->
    <template #panel-analysis>
      <NavHeader title="분석" closeable @close="closePanel" />
      <div class="panel-content">
        <p>분석 콘텐츠</p>
      </div>
    </template>

    <!-- Settings Panel -->
    <template #panel-settings>
      <NavHeader title="설정" closeable @close="closePanel" />
      <div class="panel-content">
        <p>설정 콘텐츠</p>
      </div>
    </template>

    <!-- User Panel -->
    <template #panel-user>
      <NavHeader title="사용자" closeable @close="closePanel" />
      <div class="panel-content">
        <p>사용자 정보</p>
      </div>
    </template>
  </NavigationBar>
</template>

인터랙티브 데모

주요 기능 상세

메뉴 타입 동작

MenuItem의 type 속성으로 메뉴 클릭 시 동작을 제어합니다.

  • type: 'panel': 메뉴 클릭 시 연결된 패널이 열리거나 닫힙니다
  • type: 'event': 패널을 열지 않고 menu-click 이벤트만 발생시킵니다
vue
<script setup>
const menuItems = ref([
  {
    key: 'dashboard',
    type: 'panel',  // 패널 토글
    // ... 기타 속성
  },
  {
    key: 'external-link',
    type: 'event',  // 이벤트만 발생
    // ... 기타 속성
  }
]);

const handleMenuClick = (item) => {
  if (item.type === 'event') {
    // 외부 링크 이동, 다운로드 등의 액션 수행
    window.open('https://example.com', '_blank');
  }
};
</script>

<template>
  <NavigationBar
    :head-menu-items="menuItems"
    @menu-click="handleMenuClick"
  />
</template>

스크롤 위치 유지

NavigationBar는 패널별 스크롤 위치를 자동으로 저장하고 복원합니다.

동작 방식:

  • 각 패널의 스크롤 위치가 Map에 저장됩니다
  • 다른 패널로 전환 시 현재 스크롤 위치가 저장됩니다
  • 이전에 열었던 패널로 돌아가면 스크롤 위치가 복원됩니다
  • 성능을 위해 throttle(200ms) 적용된 스크롤 핸들러 사용
  • 일반 패널과 커스텀 패널의 스크롤 위치가 독립적으로 관리됩니다
vue
<!-- 스크롤 위치는 자동으로 관리되므로 별도 설정 불필요 -->
<NavigationBar>
  <template #panel-dashboard>
    <NavHeader title="대시보드" />
    <div class="panel-content">
      <!-- 긴 콘텐츠... -->
      <!-- 스크롤 후 다른 패널로 이동했다가 돌아와도 -->
      <!-- 스크롤 위치가 자동으로 복원됩니다 -->
    </div>
  </template>
</NavigationBar>

패널 너비 설정

panelWidth prop은 일반 패널과 커스텀 패널 모두에 적용됩니다.

vue
<script setup>
const panelWidth = ref(256);  // 기본값

// 필요에 따라 동적으로 변경 가능
const expandPanel = () => {
  panelWidth.value = 400;
};
</script>

<template>
  <NavigationBar :panel-width="panelWidth" />
</template>

참고: 각 패널마다 다른 너비가 필요한 경우, CSS로 개별 패널 콘텐츠의 너비를 조정하세요.

커스텀 패널 동작

커스텀 패널은 일반 패널과 독립적으로 작동하는 특수 패널입니다.

특징:

  • enableCustomPanel prop으로 표시/숨김 제어
  • 메뉴 선택과 무관하게 독립적으로 표시 가능
  • 일반 패널이 열리면 자동으로 닫힙니다
  • 외부 클릭 시 자동으로 닫힙니다
  • 별도의 스크롤 위치 저장
vue
<script setup>
const showCustomPanel = ref(false);

const openHelp = () => {
  showCustomPanel.value = true;
};

const closeCustomPanel = () => {
  showCustomPanel.value = false;
};
</script>

<template>
  <NavigationBar :enable-custom-panel="showCustomPanel">
    <template #custom-panel>
      <NavHeader title="도움말" closeable @close="closeCustomPanel" />
      <div class="panel-content">
        <p>도움말 콘텐츠...</p>
      </div>
    </template>
  </NavigationBar>

  <!-- 외부에서 커스텀 패널 열기 -->
  <button @click="openHelp">도움말 보기</button>
</template>

Z-Index 레이어 구조

NavigationBar와 패널들의 z-index 구조를 이해하면 레이아웃 문제를 방지할 수 있습니다.

Navigation Bar: z-index 20 (최상단)
├─ General Panel: z-index 10 (중간)
└─ Custom Panel: z-index 9 (하단)

주의사항:

  • 일반 패널이 열린 상태에서 커스텀 패널을 열면, 일반 패널이 위에 표시됩니다
  • 다른 UI 요소와 z-index 충돌을 피하려면 이 구조를 고려하세요
  • Teleport를 사용하므로 부모 컨테이너의 z-index는 영향을 주지 않습니다

고급 사용법

1. 배지 표시

알림 개수 등을 배지로 표시할 수 있습니다.

vue
<script setup>
const menuItems = ref([
  {
    key: 'notification',
    icon: ICON_TYPE.bell2,
    displayName: '알림',
    badge: 3,  // 숫자 배지
    type: 'panel'
  },
  {
    key: 'messages',
    icon: ICON_TYPE.message,
    displayName: '메시지',
    badge: 'NEW',  // 텍스트 배지
    type: 'panel'
  }
]);
</script>

2. 이벤트 타입 메뉴

패널을 열지 않고 이벤트만 발생시키는 메뉴입니다.

vue
<script setup>
const menuItems = ref([
  {
    key: 'report',
    icon: ICON_TYPE.report2,
    displayName: '리포트',
    type: 'event'  // 패널을 열지 않음
  }
]);

const handleMenuClick = (item) => {
  if (item.type === 'event') {
    // 페이지 이동 또는 다른 액션 수행
    router.push(`/${item.key}`);
  }
};
</script>

<template>
  <NavigationBar
    :head-menu-items="menuItems"
    @menu-click="handleMenuClick"
  />
</template>

3. 서브메뉴 with 썸네일

서브메뉴 아이템에 썸네일 이미지를 표시할 수 있습니다.

vue
<template>
  <NavigationBar>
    <template #panel-dashboard>
      <NavHeader title="대시보드" closeable @close="closePanel" />
      <div class="panel-content">
        <SubNavItem
          v-for="item in dashboardItems"
          :key="item.key"
          :icon="item.icon"
          :name="item.displayName"
          :thumbnail="item.thumbnail"
          @click="navigateTo(item.key)"
        />
      </div>
    </template>
  </NavigationBar>
</template>

4. 커스텀 패널

일반 패널과 독립적으로 작동하는 커스텀 패널을 사용할 수 있습니다.

vue
<script setup>
const showCustomPanel = ref(false);

const openCustomPanel = () => {
  showCustomPanel.value = true;
};

const closeCustomPanel = () => {
  showCustomPanel.value = false;
};
</script>

<template>
  <NavigationBar :enable-custom-panel="showCustomPanel">
    <template #custom-panel>
      <NavHeader title="도움말" closeable @close="closeCustomPanel" />
      <div class="panel-content">
        <p>도움말 콘텐츠...</p>
      </div>
    </template>
  </NavigationBar>

  <button @click="openCustomPanel">도움말 열기</button>
</template>

5. 동적 패널 너비

패널 너비를 동적으로 조정할 수 있습니다.

vue
<script setup>
const panelWidth = ref(256);

const togglePanelWidth = () => {
  panelWidth.value = panelWidth.value === 256 ? 400 : 256;
};
</script>

<template>
  <NavigationBar :panel-width="panelWidth" />
</template>

의존성

내부 컴포넌트

  • NavHeader: 패널 헤더 컴포넌트
  • SubNavItem: 서브메뉴 아이템 컴포넌트
  • SubHeader: 서브메뉴 섹션 헤더
  • Divider: 구분선 컴포넌트
  • WrappedNavItem: 메뉴 아이템 래퍼

외부 의존성

  • @vueuse/components: vOnClickOutside 디렉티브
  • @jennifersoft/vue-components-v2: ICON_TYPE

스타일링

기본 구조

  • 네비게이션 바: 72px 고정 너비, 좌측 고정
  • 패널: 기본 256px 너비, 슬라이드 애니메이션
  • z-index: 네비게이션 바(20), 일반 패널(10), 커스텀 패널(9)

CSS 클래스

scss
.navigation-bar {
  width: 72px;
  height: 100vh;
  position: fixed;
  left: 0;
  top: 0;
}

.navigation-panel {
  position: fixed;
  left: 72px;
  top: 0;
  height: 100vh;
  background: white;
  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}

주의사항

  1. Teleport 사용: 기본적으로 body에 Teleport되므로 전역 스타일 충돌에 주의하세요.

  2. 패널 외부 클릭: 패널은 외부 클릭 시 자동으로 닫힙니다.

  3. z-index 관리: 커스텀 패널과 일반 패널은 별도의 z-index를 가집니다.

  4. v-model 사용: selectedPanel은 v-model로 양방향 바인딩됩니다.

  5. 슬롯 네이밍: 패널 슬롯은 #panel-{key} 형식으로 명명해야 합니다.

브라우저 지원

  • Chrome (최신 2개 버전)
  • Firefox (최신 2개 버전)
  • Safari (최신 2개 버전)
  • Edge (최신 2개 버전)