즐겨찾기·홀드 미리보기·페이지 도움말·3개국어 번역을 추가하고, 덱 저장 시 썸네일을 1200px로 통일하며 Nginx/API 업로드 제한을 상향했다. Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
7.6 KiB
JavaScript
241 lines
7.6 KiB
JavaScript
/**
|
|
* 헤더 프로필 드롭다운
|
|
*/
|
|
(function initAuthHeader() {
|
|
const DEFAULT_AVATAR =
|
|
'data:image/svg+xml,' +
|
|
encodeURIComponent(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#94a3b8"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>',
|
|
);
|
|
|
|
let currentUser = null;
|
|
let dropdownOpen = false;
|
|
|
|
/**
|
|
* @param {string} key
|
|
* @param {string} fallback
|
|
* @returns {string}
|
|
*/
|
|
function t(key, fallback) {
|
|
if (window.UaPageI18n?.t) {
|
|
return window.UaPageI18n.t(key, fallback);
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
/**
|
|
* HTML 이스케이프
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
function escapeHtml(str) {
|
|
return String(str ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
/**
|
|
* 드롭다운 닫기
|
|
*/
|
|
function closeDropdown() {
|
|
const dd = document.getElementById('auth-dropdown');
|
|
if (dd) dd.classList.remove('open');
|
|
dropdownOpen = false;
|
|
}
|
|
|
|
/**
|
|
* 아바타 이미지 src (캐시 무효화 포함)
|
|
* @param {object|null} user
|
|
* @returns {string}
|
|
*/
|
|
function resolveAvatarSrc(user) {
|
|
const raw = user?.avatarUrl;
|
|
if (!raw) return DEFAULT_AVATAR;
|
|
let src =
|
|
window.UaDeckApi?.resolveMediaUrl
|
|
? window.UaDeckApi.resolveMediaUrl(raw) || raw
|
|
: raw;
|
|
if (user?.updatedAt && src && !src.startsWith('data:')) {
|
|
src += (src.includes('?') ? '&' : '?') + 't=' + encodeURIComponent(user.updatedAt);
|
|
}
|
|
return src;
|
|
}
|
|
|
|
/**
|
|
* 표시용 닉네임
|
|
* @param {object|null} user
|
|
* @returns {string}
|
|
*/
|
|
function getDisplayName(user) {
|
|
if (!user) return '';
|
|
return String(user.username || user.displayName || '').trim();
|
|
}
|
|
|
|
/**
|
|
* 아바타 HTML
|
|
* @param {object|null} user
|
|
* @returns {string}
|
|
*/
|
|
function avatarHtml(user) {
|
|
const src = resolveAvatarSrc(user);
|
|
return `<img src="${src}" alt="profile" onerror="this.src='${DEFAULT_AVATAR}'" />`;
|
|
}
|
|
|
|
/**
|
|
* 프로필 버튼 HTML (아바타 + 닉네임)
|
|
* @param {object|null} user
|
|
* @returns {string}
|
|
*/
|
|
function profileBtnHtml(user) {
|
|
const name = getDisplayName(user);
|
|
const avatar = avatarHtml(user);
|
|
if (!name) {
|
|
return `<span class="auth-avatar-circle">${avatar}</span>`;
|
|
}
|
|
return `
|
|
<span class="auth-avatar-circle">${avatar}</span>
|
|
<span class="auth-username">${escapeHtml(name)}</span>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 드롭다운 메뉴 렌더
|
|
*/
|
|
function renderDropdown() {
|
|
const dd = document.getElementById('auth-dropdown');
|
|
if (!dd) return;
|
|
|
|
if (!currentUser) {
|
|
dd.innerHTML = `
|
|
<a href="/login.html" class="auth-dropdown-item">${escapeHtml(t('authLogin', '로그인'))}</a>
|
|
<a href="/signup.html" class="auth-dropdown-item">${escapeHtml(t('authSignup', '회원가입'))}</a>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const adminLink =
|
|
currentUser.role === 'admin'
|
|
? `<a href="/admin.html" class="auth-dropdown-item">${escapeHtml(t('authAdmin', '관리자'))}</a>`
|
|
: '';
|
|
|
|
dd.innerHTML = `
|
|
<div class="auth-dropdown-header">
|
|
<div class="auth-dropdown-name">${escapeHtml(getDisplayName(currentUser))}</div>
|
|
<div class="auth-dropdown-email">${escapeHtml(currentUser.email || '')}</div>
|
|
</div>
|
|
<a href="/profile.html" class="auth-dropdown-item">${escapeHtml(t('authMyProfile', '내 프로필'))}</a>
|
|
<a href="/deckHistory.html" class="auth-dropdown-item">${escapeHtml(t('authMyDecks', '내 덱 관리'))}</a>
|
|
<a href="/preset.html" class="auth-dropdown-item">${escapeHtml(t('authPublicDecks', '공개 덱'))}</a>
|
|
${adminLink}
|
|
<div class="auth-dropdown-divider"></div>
|
|
<button type="button" class="auth-dropdown-item danger" id="auth-logout-btn">${escapeHtml(t('authLogout', '로그아웃'))}</button>
|
|
`;
|
|
|
|
const logoutBtn = document.getElementById('auth-logout-btn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => {
|
|
window.UaDeckApi.logout();
|
|
currentUser = null;
|
|
closeDropdown();
|
|
location.href = '/';
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 헤더 UI 갱신
|
|
*/
|
|
function renderHeader() {
|
|
const btn = document.getElementById('auth-avatar-btn');
|
|
if (btn) {
|
|
const name = getDisplayName(currentUser);
|
|
btn.innerHTML = profileBtnHtml(currentUser);
|
|
btn.classList.toggle('auth-profile-trigger--named', Boolean(name));
|
|
btn.classList.toggle('auth-profile-trigger--guest', !name);
|
|
const menuLabel = name
|
|
? t('authAccountMenuNamed', '{name} 계정 메뉴').replace('{name}', name)
|
|
: t('authAccountMenu', '계정 메뉴');
|
|
btn.setAttribute('aria-label', menuLabel);
|
|
}
|
|
renderDropdown();
|
|
}
|
|
|
|
/**
|
|
* 슬롯에 헤더 마운트
|
|
* @param {HTMLElement} slot
|
|
*/
|
|
function mount(slot) {
|
|
slot.innerHTML = `
|
|
<div class="auth-header-wrap">
|
|
<button type="button" id="auth-avatar-btn" class="auth-profile-trigger auth-profile-trigger--guest" aria-label="${escapeHtml(t('authAccountMenu', '계정 메뉴'))}">
|
|
${profileBtnHtml(currentUser)}
|
|
</button>
|
|
<div id="auth-dropdown" class="auth-dropdown"></div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('auth-avatar-btn').addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
dropdownOpen = !dropdownOpen;
|
|
const dd = document.getElementById('auth-dropdown');
|
|
if (dropdownOpen) {
|
|
await refreshCurrentUser();
|
|
renderHeader();
|
|
dd.classList.add('open');
|
|
} else {
|
|
dd.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', closeDropdown);
|
|
}
|
|
|
|
/**
|
|
* 서버에서 최신 사용자 정보 동기화
|
|
*/
|
|
async function refreshCurrentUser() {
|
|
if (!window.UaDeckApi?.getToken()) {
|
|
currentUser = null;
|
|
return;
|
|
}
|
|
try {
|
|
const { user } = await window.UaDeckApi.getMe();
|
|
currentUser = user;
|
|
window.UaDeckApi.saveAuth(window.UaDeckApi.getToken(), user);
|
|
} catch {
|
|
window.UaDeckApi.logout();
|
|
currentUser = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 초기화
|
|
*/
|
|
async function init() {
|
|
if (window.UaPageI18n?.applyPageI18n) {
|
|
await window.UaPageI18n.applyPageI18n();
|
|
}
|
|
|
|
currentUser = window.UaDeckApi.getCachedUser();
|
|
await refreshCurrentUser();
|
|
|
|
const slot = document.getElementById('auth-header-slot');
|
|
if (slot) {
|
|
mount(slot);
|
|
renderHeader();
|
|
} else {
|
|
renderHeader();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
window.UaDeckAuth = {
|
|
getUser: () => currentUser,
|
|
refresh: init,
|
|
refreshCurrentUser,
|
|
};
|
|
})();
|