feat: refine navigation and account access flow

This commit is contained in:
2026-08-14 14:16:04 +09:00
parent cee36bf12d
commit 146e78cddf
12 changed files with 239 additions and 124 deletions
+97
View File
@@ -0,0 +1,97 @@
export function mountAppBar({ onManageAccess } = {}) {
const header = document.querySelector('#site-header');
if (!header) throw new Error('Global app bar mount point is missing.');
header.innerHTML = `
<a class="brand" href="/" aria-label="Our Recipe Atlas 홈">
<span class="brand-mark brand-mark-small" aria-hidden="true">ORA</span>
<span>Our Recipe Atlas</span>
</a>
<div class="profile">
<img id="profile-image" class="avatar" alt="" hidden>
<span id="profile-name"></span>
<button id="access-menu-button" class="menu-button" type="button"
aria-label="메뉴 열기" aria-controls="access-drawer"
aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>`;
header.insertAdjacentHTML('afterend', `
<button id="drawer-backdrop" class="drawer-backdrop" type="button"
aria-label="메뉴 닫기" hidden></button>
<aside id="access-drawer" class="access-drawer" role="dialog" aria-modal="true"
aria-labelledby="drawer-title" aria-hidden="true" hidden>
<div class="drawer-heading">
<div>
<p class="section-number">MENU</p>
<h2 id="drawer-title">메뉴</h2>
</div>
<button id="close-access-drawer" class="drawer-close" type="button"
aria-label="메뉴 닫기">×</button>
</div>
<div class="drawer-account">
<strong id="drawer-user-name"></strong>
<span id="drawer-user-email"></span>
</div>
<div class="drawer-actions">
<button id="open-access-overlay" class="drawer-action" type="button" hidden>
<span>로그인 허용 계정 관리</span><small>관리자</small>
</button>
<button id="drawer-logout" class="drawer-action danger" type="button" data-logout>
로그아웃
</button>
</div>
</aside>`);
const elements = {
menuButton: document.querySelector('#access-menu-button'),
profileImage: document.querySelector('#profile-image'),
profileName: document.querySelector('#profile-name'),
drawer: document.querySelector('#access-drawer'),
closeButton: document.querySelector('#close-access-drawer'),
backdrop: document.querySelector('#drawer-backdrop'),
manageAccessButton: document.querySelector('#open-access-overlay'),
drawerUserName: document.querySelector('#drawer-user-name'),
drawerUserEmail: document.querySelector('#drawer-user-email'),
};
function setOpen(open) {
elements.menuButton.setAttribute('aria-expanded', String(open));
elements.drawer.setAttribute('aria-hidden', String(!open));
elements.drawer.hidden = !open;
elements.backdrop.hidden = !open;
document.body.classList.toggle('drawer-open', open);
if (open) elements.closeButton.focus();
else elements.menuButton.focus();
}
elements.menuButton.addEventListener('click', () => setOpen(true));
elements.closeButton.addEventListener('click', () => setOpen(false));
elements.backdrop.addEventListener('click', () => setOpen(false));
elements.manageAccessButton.addEventListener('click', () => {
setOpen(false);
if (onManageAccess) onManageAccess();
else window.location.assign('/?manage-access=1');
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !elements.drawer.hidden) setOpen(false);
});
return {
focusMenuButton() {
elements.menuButton.focus();
},
setUser(user) {
elements.profileName.textContent = user.name || user.email;
if (user.picture) {
elements.profileImage.src = user.picture;
elements.profileImage.alt = `${user.name || user.email} 프로필`;
elements.profileImage.hidden = false;
}
elements.drawerUserName.textContent = user.name || user.email;
elements.drawerUserEmail.textContent = user.email;
elements.manageAccessButton.hidden = !user.canManageAccess;
},
};
}
+20 -43
View File
@@ -1,8 +1,11 @@
import { apiRequest } from './api.js';
import { mountAppBar } from './app-bar.js';
import { bindLogout, loadSession } from './auth.js';
import { searchRecipes } from './recipe-search.js';
import { bindScrollToTop } from './scroll-to-top.js';
const appBar = mountAppBar({ onManageAccess: openAccessManager });
const elements = {
importForm: document.querySelector('#import-form'),
importStatus: document.querySelector('#import-status'),
@@ -17,13 +20,6 @@ const elements = {
tagFilterOptions: document.querySelector('#tag-filter-options'),
toggleTagFilter: document.querySelector('#toggle-tag-filter'),
clearTagFilter: document.querySelector('#clear-tag-filter'),
accessMenuButton: document.querySelector('#access-menu-button'),
accessDrawer: document.querySelector('#access-drawer'),
closeAccessDrawer: document.querySelector('#close-access-drawer'),
drawerBackdrop: document.querySelector('#drawer-backdrop'),
drawerUserName: document.querySelector('#drawer-user-name'),
drawerUserEmail: document.querySelector('#drawer-user-email'),
openAccessOverlay: document.querySelector('#open-access-overlay'),
accessOverlay: document.querySelector('#access-overlay'),
closeAccessOverlay: document.querySelector('#close-access-overlay'),
accessOverlayBackdrop: document.querySelector('#access-overlay-backdrop'),
@@ -199,20 +195,7 @@ async function loadAllowedEmails() {
}
function updatePageLock() {
document.body.classList.toggle(
'drawer-open',
!elements.accessDrawer.hidden || !elements.accessOverlay.hidden,
);
}
function setAccessDrawerOpen(open) {
elements.accessMenuButton.setAttribute('aria-expanded', String(open));
elements.accessDrawer.setAttribute('aria-hidden', String(!open));
elements.accessDrawer.hidden = !open;
elements.drawerBackdrop.hidden = !open;
updatePageLock();
if (open) elements.closeAccessDrawer.focus();
else elements.accessMenuButton.focus();
document.body.classList.toggle('drawer-open', !elements.accessOverlay.hidden);
}
function setAccessOverlayOpen(open) {
@@ -221,7 +204,16 @@ function setAccessOverlayOpen(open) {
elements.accessOverlayBackdrop.hidden = !open;
updatePageLock();
if (open) elements.accessEmail.focus();
else elements.accessMenuButton.focus();
else appBar.focusMenuButton();
}
async function openAccessManager() {
setAccessOverlayOpen(true);
try {
await loadAllowedEmails();
} catch (error) {
showToast(error.message);
}
}
function ingredientGroupTemplate(group, groupIndex) {
@@ -444,28 +436,11 @@ elements.clearRecipeSearch.addEventListener('click', () => {
renderRecipes();
});
elements.accessMenuButton.addEventListener('click', () => {
setAccessDrawerOpen(true);
});
elements.openAccessOverlay.addEventListener('click', async () => {
setAccessDrawerOpen(false);
setAccessOverlayOpen(true);
try {
await loadAllowedEmails();
} catch (error) {
showToast(error.message);
}
});
elements.closeAccessDrawer.addEventListener('click', () => setAccessDrawerOpen(false));
elements.drawerBackdrop.addEventListener('click', () => setAccessDrawerOpen(false));
elements.closeAccessOverlay.addEventListener('click', () => setAccessOverlayOpen(false));
elements.accessOverlayBackdrop.addEventListener('click', () => setAccessOverlayOpen(false));
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (!elements.accessOverlay.hidden) setAccessOverlayOpen(false);
else if (!elements.accessDrawer.hidden) setAccessDrawerOpen(false);
});
elements.accessForm.addEventListener('submit', async (event) => {
@@ -507,9 +482,11 @@ elements.allowedEmailList.addEventListener('click', async (event) => {
bindLogout();
bindScrollToTop(elements.scrollToTop);
currentUser = await loadSession();
elements.drawerUserName.textContent = currentUser.name || currentUser.email;
elements.drawerUserEmail.textContent = currentUser.email;
appBar.setUser(currentUser);
await loadRecipes();
if (currentUser.canManageAccess) {
elements.openAccessOverlay.hidden = false;
if (
currentUser.canManageAccess
&& new URLSearchParams(window.location.search).get('manage-access') === '1'
) {
await openAccessManager();
}
+7
View File
@@ -0,0 +1,7 @@
const error = new URLSearchParams(window.location.search).get('error');
if (error === 'account-not-allowed') {
document.querySelector('#login-alert').hidden = false;
document.querySelector('#google-login').href = '/auth/google/select-account';
document.querySelector('#google-login-label').textContent = '다른 Google 계정으로 다시 시도';
}
+3 -1
View File
@@ -1,7 +1,9 @@
import { apiRequest } from './api.js';
import { mountAppBar } from './app-bar.js';
import { bindLogout, loadSession } from './auth.js';
import { bindScrollToTop } from './scroll-to-top.js';
const appBar = mountAppBar();
const detail = document.querySelector('#recipe-detail');
const toast = document.querySelector('#toast');
const scrollToTop = document.querySelector('#scroll-to-top');
@@ -221,7 +223,7 @@ function renderRecipe(recipe) {
}
bindLogout();
await loadSession();
appBar.setUser(await loadSession());
try {
const recipeId = decodeURIComponent(window.location.pathname.split('/').filter(Boolean).pop());