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와 함께 사용되는 주요 컴포넌트들입니다.

모바일 GNB는 MobileNavigation을 참고하세요.

좌측 레일에 표시되는 최상위 메뉴 항목입니다. NavigationBar가 내부에서 사용하며, 레일을 직접 조립할 때만 개별 사용합니다.

Props

Prop타입설명
iconIconTypes항목 아이콘 (필수)
namestring항목 이름 (필수)
selectedboolean?현재 선택된 항목
pressedboolean?눌린 상태(패널이 열려 있는 동안)
disabledboolean?비활성
badgenumber | string?배지 값
tooltipstring?축소 상태에서 노출할 툴팁

이벤트

typescript
emit('click', event: MouseEvent)

selectedpressed는 다릅니다. selected현재 화면이 그 메뉴에 속함을, pressed그 메뉴의 패널이 열려 있음을 뜻합니다. 다른 메뉴를 눌러 패널만 열어둔 경우 두 값이 서로 다른 항목에 붙습니다.

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

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개 버전)