내비게이션
웹에서 하단 탭 바, 사이드 드로어, 상단 드롭다운 메뉴, 하단 시트를 제어합니다.
한눈에 보기
네비게이션 Bridge를 사용하면 앱 내 모든 네이티브 패널을 웹 페이지에서 직접 제어할 수 있습니다.
| 컴포넌트 | Bridge 네임스페이스 | config.json 키 |
|---|---|---|
| Bottom Tabs | unveilyBridge.bottomTabs | modules.bottomTabs.enabled |
| Side Drawer | unveilyBridge.sideDrawer | modules.sideDrawer.enabled |
| Top Down Menu | unveilyBridge.topDownMenu | modules.topDownMenu.enabled |
| Bottom Sheet | unveilyBridge.bottomSheet | modules.bottomSheet.enabled |
패널 상호 배타성
Bottom Sheet, Top Down Menu, Side Drawer는 동시에 하나만 열립니다. 새 패널을 열면 열려 있던 다른 패널은 자동으로 닫힙니다.
타이밍 — unveilyGlueReady 대기
window.unveilyBridge는 페이지 로드 전에 등록되지만, 모듈 네임스페이스(bottomTabs, sideDrawer 등)는 네이티브 쪽의 onPageFinished 이후에 글루 스크립트가 주입되어야 생성됩니다.
React, Vue 등 SPA에서 useEffect 또는 onMounted는 글루 주입 이전에 실행될 수 있어, setConfig() 같은 호출이 조용히 무시될 수 있습니다.
마운트 시점에 브릿지를 호출하는 경우, 반드시 unveilyGlueReady 이벤트를 기다린 후 호출하세요:
window.addEventListener('unveilyGlueReady', () => {
window.unveilyBridge.bottomTabs.setConfig({ items: [...] });
window.unveilyBridge.sideDrawer.setConfig({ items: [...] });
}, { once: true });버튼 클릭 등 사용자 동작으로 호출하는 경우에는 이 처리가 필요 없습니다 — 사용자가 조작하는 시점에는 글루가 항상 준비되어 있습니다.
모듈 활성화
배포되는 config.json은 모든 UI 모듈이 비활성(opt-in) 상태로 제공됩니다. 앱에서 실제로 쓰는 패널만 켜세요.
필요한 것만 켜기
요청하지 않은 탭·드로어·제스처가 앱에 주입되지 않도록, 그리고 불필요한 권한·스토어 심사 이슈를 피하도록 모듈은 기본적으로 꺼져 있습니다. 사용할 컴포넌트만 "enabled": true로 바꾸세요.
{
"modules": {
"bottomTabs": { "enabled": true, "autoHide": false, "heightDp": 60 },
"sideDrawer": { "enabled": true },
"topDownMenu": { "enabled": true },
"bottomSheet": { "enabled": true }
}
}빠르게 둘러보려면 모든 패널이 켜진 데모 설정 config.sample.json이 함께 제공됩니다. config.json을 백업한 뒤 교체해 보고, 끝나면 되돌리세요. (Android: app/src/main/assets/, iOS: Resources/)
cp config.json config.json.bak && cp config.sample.json config.json로컬 config로 티어(플랜)를 판단하지 마세요
플랜 게이팅은 원격/라이선스 경로에서만 적용됩니다. 로컬 config.json은 이를 우회하므로, 로컬 데모에서는 실제 플랜에서 막히는 패널도 보일 수 있습니다. 프로덕션 가용성은 플랜 기준으로 확인하세요.
패널 색상 커스터마이징
각 모듈에 panelColor 옵션을 설정하면 앱 브랜드 컬러로 패널 배경색을 지정할 수 있습니다. config.json에 한 번만 설정하면 SDK 업데이트 후에도 유지됩니다.
단일 색상 (라이트/다크 공통 적용):
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": "#CC1A1A2E"
}
}
}라이트/다크 모드 분리 (시스템 테마에 따라 자동 전환):
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": {
"light": "#CCF0F0F5",
"dark": "#CC1A1A2E"
}
},
"sideDrawer": {
"enabled": true,
"panelColor": {
"light": "#CCFFFFFF",
"dark": "#CC2C2C2C"
}
},
"topDownMenu": {
"enabled": true,
"panelColor": "#CC101020"
},
"bottomSheet": {
"enabled": true,
"panelColor": "#CC1C1C1E"
}
}
}색상 형식: "#AARRGGBB" (알파 포함) 또는 "#RRGGBB" (불투명). panelColor를 생략하면 SDK 기본 배경이 사용됩니다.
우선순위
panelColor는 접근성 패널의 High Contrast 설정보다 우선 적용됩니다. High Contrast를 지원하려면 panelColor를 설정하지 않거나, 접근성 설정 변경 시 JS에서 동적으로 색상을 조정하세요.
아이콘 / 라벨 색상
iconColor와 labelColor로 아이콘 틴트와 라벨 글자색을 별도 지정할 수 있습니다. Bottom Tabs, Top Down Menu, Side Drawer에서 지원하며, panelColor와 동일한 단일 색상 / {light, dark} 객체 형식을 사용합니다.
{
"modules": {
"bottomTabs": {
"enabled": true,
"panelColor": "#CC1A1A2E",
"iconColor": { "light": "#1A1A2E", "dark": "#FFFFFF" },
"labelColor": { "light": "#1A1A2E", "dark": "#FFFFFF" },
"activeAlpha": 1.0,
"inactiveAlpha": 0.5
},
"sideDrawer": {
"enabled": true,
"panelColor": "#CC2C2C2C",
"iconColor": "#FFFFFF",
"labelColor": "#FFFFFF"
},
"topDownMenu": {
"enabled": true,
"panelColor": "#CC101020",
"iconColor": "#FFFFFF",
"labelColor": "#CCCCCC"
}
}
}| 옵션 | 적용 대상 | 기본값 |
|---|---|---|
iconColor | 아이콘 ImageView 틴트 (SRC_IN 방식) | 원본 이미지 색상 |
labelColor | 라벨 TextView 글자색 | #FFFFFF (흰색) |
activeAlpha | Bottom Tabs 활성 탭 불투명도 | 1.0 |
inactiveAlpha | Bottom Tabs 비활성 탭 불투명도 | 0.6 |
아이콘 틴트
iconColor는 단색 아이콘(SVG, 단색 PNG)에 최적입니다. 멀티컬러 아이콘에 적용하면 단색으로 채워집니다.
활성 탭 인디케이터
activeIndicator로 현재 활성 탭에 시각적 마커를 추가할 수 있습니다. activeIndicatorColor를 지정하지 않으면 iconColor가 대신 사용됩니다.
"bottomTabs": {
"enabled": true,
"panelColor": "#E8101020",
"iconColor": { "light": "#1F2937", "dark": "#FFFFFF" },
"activeIndicator": "dot",
"activeIndicatorColor": { "light": "#2563EB", "dark": "#60A5FA" }
}| 옵션 | 설명 | 기본값 |
|---|---|---|
activeIndicator | 인디케이터 스타일 — "none" / "dot" / "pill" / "bar" | "none" |
activeIndicatorColor | 인디케이터 색상 (panelColor와 동일한 단일/객체 형식) | #6366F1 / #818CF8 (Indigo) |
activeIndicatorWidth | "bar" 너비 — "short" (20dp 중앙 정렬) / "full" (탭 전체 너비) | "short" |
activeIndicatorPosition | "bar" 위치 — "top" (아이콘 위) / "bottom" (라벨 아래) | "top" |
dot— 라벨 아래 작은 원형 점bar— 아이콘 위 짧은 가로 막대pill— 아이콘과 라벨을 감싸는 둥근 직사각형 배경
pill 투명도
"pill" 스타일에서는 activeIndicatorColor에 알파 채널을 포함하세요 (예: "#332563EB" ≈ 20% 불투명도). 불투명한 색상을 사용하면 아이콘과 라벨이 가려집니다.
Bottom Tabs
화면 하단에 고정된 탭 바입니다.
탭바 높이 설정
config.json의 heightDp 옵션으로 탭 바 높이를 조정할 수 있습니다.
{
"modules": {
"bottomTabs": {
"enabled": true,
"heightDp": 64
}
}
}| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
heightDp | number | 54 | 탭 바 높이(dp). 44–96 범위로 자동 제한됩니다. |
접근성 최솟값
44dp 미만으로 설정해도 44dp로 올림 처리됩니다 (WCAG 2.5.5 최소 터치 타겟 기준). 96dp를 초과하면 96dp로 내림 처리됩니다.
show / hide / toggle
window.unveilyBridge.bottomTabs.show();
window.unveilyBridge.bottomTabs.hide();
window.unveilyBridge.bottomTabs.toggle();setConfig
window.unveilyBridge.bottomTabs.setConfig({
visible: false, // 선택 — 기본값: true. false 로 설정하면 최초 로드 시 숨긴 상태로 시작합니다.
items: [
{ id: "home", label: "홈", icon: "home.svg", url: "/home" },
{ id: "search", label: "검색", icon: "search.svg", url: "/search" },
{ id: "my", label: "마이", icon: "person.svg", url: "/my" }
]
});최초 setConfig() 호출 시 탭 바가 자동으로 표시됩니다. visible: false를 전달하면 숨긴 상태로 시작하며, 원하는 시점에 show()를 호출해 표시합니다. 이후 setConfig() 재호출은 표시 상태를 변경하지 않습니다.
아이콘: assets/menu_icons/에 아이콘 파일을 넣고 파일명으로 참조합니다. .svg와 래스터 포맷(.png, .webp 등)을 모두 지원합니다. 로컬 .svg 파일은 AndroidSVG로 렌더링되어 모든 해상도에서 선명하게 표시되며, 그 외 포맷은 Glide를 사용합니다.
setBadge
특정 탭에 알림 배지를 표시합니다. 0을 전달하면 제거됩니다.
window.unveilyBridge.bottomTabs.setBadge("home", 5); // 배지 표시
window.unveilyBridge.bottomTabs.setBadge("home", 0); // 배지 제거콜백
function onBottomTabClick(result) {
console.log(result.id); // 탭 id
console.log(result.url); // 해당 탭에 설정된 url
}Side Drawer
화면 측면에서 슬라이드해 들어오는 네비게이션 목록입니다.
open / close / toggle
window.unveilyBridge.sideDrawer.open();
window.unveilyBridge.sideDrawer.close();
window.unveilyBridge.sideDrawer.toggle();setConfig
window.unveilyBridge.sideDrawer.setConfig({
widthDp: 320, // 선택 사항 — 드로어 너비(dp), 기본값: 320
items: [
{ id: "profile", label: { ko: "프로필", en: "Profile", ja: "プロフィール" }, icon: "person.png", url: "/profile" },
{ id: "settings", label: { ko: "설정", en: "Settings", ja: "設定" }, icon: "settings.png", url: "/settings" },
{ id: "refresh", type: "button", label: { ko: "새로고침", en: "Refresh", ja: "更新" } },
{ id: "lang", type: "select", value: "ko", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]}
]
});setItemEnabled / setItemVisible
window.unveilyBridge.sideDrawer.setItemEnabled("settings", false);
window.unveilyBridge.sideDrawer.setItemVisible("settings", false);setBadge
아이콘 타입 항목에 알림 배지를 표시합니다. 0을 전달하면 제거됩니다.
window.unveilyBridge.sideDrawer.setBadge("profile", 3);
window.unveilyBridge.sideDrawer.setBadge("profile", 0); // 제거콜백
// 아이콘 타입 항목 탭 (하위 호환)
function onSideDrawerItemClick(result) {
console.log(result.id); // 항목 id
console.log(result.url); // 해당 항목의 url
}
// 버튼 또는 셀렉트 항목 조작
function onSideDrawerAction(result) {
console.log(result.id); // 항목 id
console.log(result.type); // "button" | "select"
console.log(result.value); // 버튼이면 ""; 셀렉트이면 선택된 값
}Top Down Menu
화면 상단에서 아래로 슬라이드하는 그리드 메뉴입니다.
open / close / toggle
window.unveilyBridge.topDownMenu.open();
window.unveilyBridge.topDownMenu.close();
window.unveilyBridge.topDownMenu.toggle();setConfig
window.unveilyBridge.topDownMenu.setConfig({
columns: 4, // 그리드 열 수 (1–5, 기본값: 4)
maxHeightFraction: 0.5, // 최대 높이 비율 — 상한 (0.1–1.0, 기본값: 0.5). 내용이 적으면 상한보다 작게 표시
items: [
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" },
{ id: "search", label: { ko: "검색", en: "Search", ja: "検索" }, icon: "search.png" },
{ id: "refresh", type: "button", label: { ko: "새로고침", en: "Refresh", ja: "更新" } },
{ id: "lang", type: "select", value: "ko", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]},
{ id: "banner", type: "image", icon: "promo.svg", heightDp: 80 }
]
});열(column) 동작
columns는 type:'icon' 그리드 셀에만 적용됩니다. button/select/toggle/image/header/separator 항목은 항상 전체 너비 행으로 표시됩니다. 또한 columns는 고정값이라 태블릿에서 열 수가 자동으로 늘지 않고 각 열이 가로로 넓어집니다. 태블릿에서 열을 더 두려면 app.isTablet()로 분기하세요.
var cols = window.unveilyBridge.app.isTablet() ? 5 : 3;
window.unveilyBridge.topDownMenu.setConfig({ columns: cols, items: [ /* ... */ ] });setItemEnabled / setItemVisible
window.unveilyBridge.topDownMenu.setItemEnabled("search", false);
window.unveilyBridge.topDownMenu.setItemVisible("search", false);닫기 제스처
- 위로 스와이프: 메뉴가 열린 상태에서 빠르게 위로 밀면 닫힙니다.
- 뒤로 가기 버튼: 앱이 종료되지 않고 메뉴를 먼저 닫습니다.
콜백
// 아이콘 타입 항목 탭 (하위 호환)
function onTopDownMenuItemClick(result) {
console.log(result.id); // 항목 id
console.log(result.url); // 해당 항목의 url
}
// 버튼 또는 셀렉트 항목 조작
function onTopDownMenuAction(result) {
console.log(result.id); // 항목 id
console.log(result.type); // "button" | "select"
console.log(result.value); // 버튼이면 ""; 셀렉트이면 선택된 값
}Bottom Sheet
화면 아래에서 위로 슬라이드하는 패널입니다. 하단 탭 바와 함께 사용할 수 있습니다.
show
window.unveilyBridge.bottomSheet.show({
title: "상품 상세", // 선택 사항
content: "설명 텍스트", // 선택 사항
heightFraction: 0.5, // 화면 높이 비율 (0.1–1.0, 기본값: 0.5)
heightDp: 400 // 고정 높이(dp) — heightFraction보다 우선
});hide / toggle
window.unveilyBridge.bottomSheet.hide();
window.unveilyBridge.bottomSheet.toggle();setConfig
setConfig로 시트 높이를 설정합니다. 열려 있으면 즉시 반영(라이브 리사이즈), 닫혀 있으면 다음 show() 호출 시 반영됩니다.
// 닫힌 상태 — 다음 show() 때 높이 적용
window.unveilyBridge.bottomSheet.setConfig({ heightFraction: 0.7 });
// 열린 상태에서 호출 — 즉시 라이브 리사이즈
window.unveilyBridge.bottomSheet.setConfig({ heightFraction: 0.3 });패널 크기 설정 공통 규칙
상단 메뉴·사이드 드로어·바텀 시트 3종 모두 크기는 setConfig가 담당합니다. 열린 상태에서 호출하면 즉시 반영되고, 닫힌 상태에서는 다음 open()/show() 시 반영됩니다. open()/show()는 표시만 담당하며, 이미 열린 패널에 다시 호출해도 크기는 바뀌지 않습니다.
크기 옵션: 상단 메뉴 maxHeightFraction(상한) · 사이드 드로어 widthDp(고정 너비) · 바텀 시트 heightFraction 또는 heightDp(고정 높이, heightDp 우선).
닫기 제스처
- 아래로 스와이프: 시트가 열린 상태에서 빠르게 아래로 밀면 닫힙니다.
- 뒤로 가기 버튼: 앱이 종료되지 않고 시트를 먼저 닫습니다.
- 바깥 영역 탭: 시트 위의 어두운 스크림을 탭하면 닫힙니다.
항목 타입
Top Down Menu와 Side Drawer의 항목은 type 필드로 렌더링 방식을 결정합니다.
type | 렌더링 | 조작 시 패널 닫힘 | 콜백 |
|---|---|---|---|
"icon" | 아이콘 + 라벨 그리드/목록 셀 (기본값) | 예 | onTopDownMenuItemClick / onSideDrawerItemClick |
"image" | 전체 너비 이미지 행 (SVG 권장) | url 설정 시만 | onTopDownMenuItemClick / onSideDrawerItemClick |
"button" | 전체 너비 네이티브 버튼 | 아니오 | onTopDownMenuAction / onSideDrawerAction |
"select" | 전체 너비 네이티브 스피너(드롭다운) | 아니오 | onTopDownMenuAction / onSideDrawerAction |
"toggle" | 전체 너비 스위치 (라벨 + 온/오프) | 아니오 | onTopDownMenuAction / onSideDrawerAction |
"separator" | 가로 구분선 | — | 없음 |
"header" | 비대화형 섹션 제목 라벨 | — | 없음 |
"button", "select", "toggle"은 조작 시 패널을 자동으로 닫지 않습니다. JS 콜백에서 직접 처리하세요.
type: "icon" (기본값)
{
"id": "settings",
"label": { "ko": "설정", "en": "Settings", "ja": "設定" },
"icon": "settings.png",
"url": "/settings"
}label은 단일 언어 환경에서 일반 문자열로도 사용할 수 있습니다: "label": "설정"
type: "image"
전체 너비 이미지 행입니다. 로컬 .svg 파일은 AndroidSVG로 렌더링되고 다른 포맷은 Glide를 사용합니다.
{
"id": "banner",
"type": "image",
"icon": "promo.svg",
"heightDp": 80,
"url": "/promo"
}| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
icon | string | — | 에셋 파일명(예: "banner.svg") 또는 원격 URL |
heightDp | number | 80 | 행 높이(dp) (20–400) |
url | string | — | 선택 사항. 설정 시 탭하면 onTopDownMenuItemClick을 실행하고 패널을 닫음 |
SVG 파일은 assets/menu_icons/에 넣고 파일명으로 참조합니다.
type: "button"
전체 너비 네이티브 버튼입니다. 클릭하면 onTopDownMenuAction / onSideDrawerAction이 실행됩니다.
{
"id": "refresh",
"type": "button",
"label": { "ko": "새로고침", "en": "Refresh", "ja": "更新" }
}type: "select"
전체 너비 네이티브 스피너(드롭다운)입니다. 옵션을 선택하면 onTopDownMenuAction / onSideDrawerAction이 실행됩니다.
{
"id": "lang",
"type": "select",
"value": "ko",
"options": [
{ "value": "ko", "label": { "ko": "한국어", "en": "Korean", "ja": "韓国語" } },
{ "value": "en", "label": { "ko": "영어", "en": "English", "ja": "英語" } },
{ "value": "ja", "label": { "ko": "일본어", "en": "Japanese", "ja": "日本語" } }
]
}| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | string | — | 미리 선택할 옵션 값 |
options | array | — | { value, label } 객체 배열 |
type: "toggle"
라벨이 왼쪽, 토글 스위치가 오른쪽에 위치하는 전체 너비 스위치입니다. value: "true" 또는 "false"로 onTopDownMenuAction / onSideDrawerAction을 실행합니다.
{
"id": "dark",
"type": "toggle",
"label": { "ko": "다크 모드", "en": "Dark Mode", "ja": "ダークモード" },
"value": false
}| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | string | i18n | — | 스위치 왼쪽에 표시할 설명 텍스트 |
value | boolean | false | 초기 체크 상태 |
type: "separator"
항목 그룹화를 위한 가로 구분선입니다. id와 콜백이 없습니다.
{ "type": "separator" }type: "header"
비대화형 섹션 제목 라벨입니다. 콜백 없음.
{
"type": "header",
"label": { "ko": "설정", "en": "Settings", "ja": "設定" }
}title 필드
button, select, toggle, image 항목은 선택 사항인 title 필드를 지원합니다.
설정하면 해당 컨트롤 바로 위에 작은 보조 텍스트 라벨이 렌더링됩니다.
일반 문자열 또는 i18n 객체 모두 지원합니다.
{
"id": "dark",
"type": "toggle",
"title": { "ko": "화면", "en": "Appearance", "ja": "画面" },
"label": { "ko": "다크 모드", "en": "Dark Mode", "ja": "ダークモード" },
"value": false
}다국어 라벨
모든 항목의 label 필드는 일반 문자열 또는 ko, en, ja 키를 가진 i18n 객체를 모두 지원합니다.
// 일반 문자열 — 언어와 무관하게 동일한 라벨 표시
{ id: "home", label: "홈", icon: "home.png" }
// i18n 객체 — setLocale()에 따라 라벨이 전환됨
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" }i18n 객체를 사용하면 현재 로케일에 맞는 라벨이 표시됩니다 (아래 setLocale 참고).
폴백 순서: 요청된 로케일 → en → 항목 id
setLocale
메뉴 항목 라벨 표시에 사용할 로케일을 재정의합니다. 앱에서 사용자가 언어를 변경할 때 호출하세요.
window.unveilyBridge.app.setLocale("ko"); // "ko" | "en" | "ja"
window.unveilyBridge.app.setLocale(""); // 기기 로케일로 초기화열려 있는 패널의 라벨은 다시 열지 않아도 즉시 업데이트됩니다.
예시 — Top Down Menu에서 언어 선택기 구현
window.unveilyBridge.topDownMenu.setConfig({
items: [
{ id: "home", label: { ko: "홈", en: "Home", ja: "ホーム" }, icon: "home.png" },
{ id: "lang", type: "select", value: "ko", options: [
{ value: "ko", label: { ko: "한국어", en: "Korean", ja: "韓国語" } },
{ value: "en", label: { ko: "영어", en: "English", ja: "英語" } },
{ value: "ja", label: { ko: "일본어", en: "Japanese", ja: "日本語" } }
]}
]
});
function onTopDownMenuAction(result) {
if (result.id === "lang" && result.type === "select") {
window.unveilyBridge.app.setLocale(result.value);
}
}