v4.0.0: 덱 UX·i18n·썸네일 저장 개선 및 413 수정
즐겨찾기·홀드 미리보기·페이지 도움말·3개국어 번역을 추가하고, 덱 저장 시 썸네일을 1200px로 통일하며 Nginx/API 업로드 제한을 상향했다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
163
profile.html
163
profile.html
@@ -26,13 +26,15 @@
|
||||
<div class="app-body-layout">
|
||||
<main class="app-main profile-content">
|
||||
<header class="profile-header">
|
||||
<h1>내 프로필</h1>
|
||||
<p>프로필 사진, 사용자명, 이메일, 비밀번호를 변경할 수 있습니다.</p>
|
||||
<div class="page-title-row">
|
||||
<h1 data-i18n="profileTitle">내 프로필</h1>
|
||||
<button type="button" class="page-help-btn" data-i18n-tooltip="profileDesc" aria-expanded="false">?</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="profile-loading" class="profile-card">
|
||||
<div class="profile-body">
|
||||
<p style="margin: 0; color: #64748b; font-size: 0.875rem">불러오는 중...</p>
|
||||
<p data-i18n="loadingLabel" style="margin: 0; color: #64748b; font-size: 0.875rem">불러오는 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,15 +56,15 @@
|
||||
|
||||
<div class="profile-body">
|
||||
<div class="profile-field">
|
||||
<label class="profile-label" for="profile-email">이메일</label>
|
||||
<label class="profile-label" for="profile-email" data-i18n="emailLabel">이메일</label>
|
||||
<input type="email" id="profile-email" class="profile-input" autocomplete="email" />
|
||||
</div>
|
||||
<div class="profile-field">
|
||||
<label class="profile-label" for="profile-username">사용자명</label>
|
||||
<label class="profile-label" for="profile-username" data-i18n="usernameLabel">사용자명</label>
|
||||
<input type="text" id="profile-username" class="profile-input" autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<button type="button" id="profile-save" class="profile-btn profile-btn-primary" disabled>
|
||||
<button type="button" id="profile-save" class="profile-btn profile-btn-primary" disabled data-i18n="profileSave">
|
||||
프로필 저장
|
||||
</button>
|
||||
<p id="profile-error" class="profile-error hidden"></p>
|
||||
@@ -70,34 +72,45 @@
|
||||
|
||||
<div class="profile-divider"></div>
|
||||
|
||||
<h3 class="profile-section-title">덱 저장 설정</h3>
|
||||
<p class="profile-section-desc">「내 덱으로 저장」시 공개 여부 기본값과 확인 방식을 설정합니다.</p>
|
||||
|
||||
<div class="profile-toggle-row">
|
||||
<div>
|
||||
<div class="profile-toggle-label">기본 공개</div>
|
||||
<div class="profile-toggle-hint">저장 시 기본으로 공개 덱에 노출</div>
|
||||
</div>
|
||||
<button type="button" id="pref-public-toggle" class="profile-toggle" aria-pressed="true">
|
||||
<span id="pref-public-circle" class="profile-toggle-circle is-on"></span>
|
||||
</button>
|
||||
<input type="checkbox" id="pref-public-check" class="hidden" checked />
|
||||
<div class="profile-section-head">
|
||||
<h3 class="profile-section-title" data-i18n="profileDeckSettings">덱 저장 설정</h3>
|
||||
<button type="button" class="page-help-btn" data-i18n-tooltip="profileDeckSettingsDesc" aria-expanded="false">?</button>
|
||||
</div>
|
||||
|
||||
<div class="profile-toggle-row">
|
||||
<div>
|
||||
<div class="profile-toggle-label">저장 시 공개 여부 물어보기</div>
|
||||
<div class="profile-toggle-hint">끄면 설정한 기본값으로 바로 저장</div>
|
||||
<div class="profile-settings-panel">
|
||||
<div class="profile-toggle-row">
|
||||
<label class="profile-toggle-text" for="pref-public-toggle">
|
||||
<span class="profile-toggle-label" data-i18n="profileDefaultPublic">기본 공개</span>
|
||||
<span class="profile-toggle-hint" data-i18n="profileDefaultPublicHint">저장 시 기본으로 공개 덱에 노출</span>
|
||||
</label>
|
||||
<div class="profile-toggle-wrap">
|
||||
<span id="pref-public-state" class="profile-toggle-state is-on" data-i18n-on="profileSettingOn" data-i18n-off="profileSettingOff">ON</span>
|
||||
<button type="button" id="pref-public-toggle" class="profile-toggle is-on" aria-pressed="true">
|
||||
<span id="pref-public-circle" class="profile-toggle-circle is-on"></span>
|
||||
</button>
|
||||
<input type="checkbox" id="pref-public-check" class="hidden" checked />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-toggle-row">
|
||||
<label class="profile-toggle-text" for="pref-ask-toggle">
|
||||
<span class="profile-toggle-label" data-i18n="profileAskOnSave">저장 시 공개 여부 물어보기</span>
|
||||
<span class="profile-toggle-hint" data-i18n="profileAskOnSaveHint">끄면 설정한 기본값으로 바로 저장</span>
|
||||
</label>
|
||||
<div class="profile-toggle-wrap">
|
||||
<span id="pref-ask-state" class="profile-toggle-state is-on" data-i18n-on="profileSettingOn" data-i18n-off="profileSettingOff">ON</span>
|
||||
<button type="button" id="pref-ask-toggle" class="profile-toggle is-on" aria-pressed="true">
|
||||
<span id="pref-ask-circle" class="profile-toggle-circle is-on"></span>
|
||||
</button>
|
||||
<input type="checkbox" id="pref-ask-check" class="hidden" checked />
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="pref-ask-toggle" class="profile-toggle" aria-pressed="true">
|
||||
<span id="pref-ask-circle" class="profile-toggle-circle is-on"></span>
|
||||
</button>
|
||||
<input type="checkbox" id="pref-ask-check" class="hidden" checked />
|
||||
</div>
|
||||
<p id="profile-prefs-notice" class="profile-prefs-notice" aria-live="polite"></p>
|
||||
|
||||
<div class="profile-divider"></div>
|
||||
|
||||
<button type="button" id="profile-open-password" class="profile-btn profile-btn-outline">
|
||||
<button type="button" id="profile-open-password" class="profile-btn profile-btn-outline" data-i18n="changePassword">
|
||||
비밀번호 변경
|
||||
</button>
|
||||
</div>
|
||||
@@ -105,35 +118,47 @@
|
||||
</main>
|
||||
|
||||
<aside class="app-ad-rail" aria-label="광고">
|
||||
<div class="app-ad-slot app-ad-slot--skyscraper" id="ad-sidebar"></div>
|
||||
<div class="app-ad-slot app-ad-slot--skyscraper" id="ad-sidebar">
|
||||
<ins
|
||||
class="adsbygoogle"
|
||||
style="display: block"
|
||||
data-ad-client="ca-pub-4516420168710424"
|
||||
data-ad-slot="4260332296"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"
|
||||
></ins>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="password-modal" class="profile-modal">
|
||||
<div class="profile-modal-box">
|
||||
<h3>비밀번호 변경</h3>
|
||||
<h3 data-i18n="changePassword">비밀번호 변경</h3>
|
||||
<div class="profile-field">
|
||||
<label class="profile-label" for="password-current">현재 비밀번호</label>
|
||||
<label class="profile-label" for="password-current" data-i18n="passwordLabel">현재 비밀번호</label>
|
||||
<input type="password" id="password-current" class="profile-input" autocomplete="current-password" />
|
||||
</div>
|
||||
<div class="profile-field">
|
||||
<label class="profile-label" for="password-new">새 비밀번호</label>
|
||||
<label class="profile-label" for="password-new" data-i18n="newPasswordLabel">새 비밀번호</label>
|
||||
<input type="password" id="password-new" class="profile-input" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="profile-field">
|
||||
<label class="profile-label" for="password-confirm">새 비밀번호 확인</label>
|
||||
<label class="profile-label" for="password-confirm" data-i18n="confirmPasswordLabel">새 비밀번호 확인</label>
|
||||
<input type="password" id="password-confirm" class="profile-input" autocomplete="new-password" />
|
||||
</div>
|
||||
<p id="password-error" class="profile-error hidden"></p>
|
||||
<div class="profile-modal-actions">
|
||||
<button type="button" id="password-cancel" class="profile-btn profile-btn-outline">취소</button>
|
||||
<button type="button" id="password-save" class="profile-btn profile-btn-primary">변경</button>
|
||||
<button type="button" id="password-cancel" class="profile-btn profile-btn-outline" data-i18n="cancel">취소</button>
|
||||
<button type="button" id="password-save" class="profile-btn profile-btn-primary" data-i18n="changePasswordButton">변경</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/script/api.js"></script>
|
||||
<script src="/script/ads.js"></script>
|
||||
<script src="/script/page-i18n.js"></script>
|
||||
<script src="/script/page-help.js"></script>
|
||||
<script src="/script/deck-prefs.js"></script>
|
||||
<script src="/script/image-utils.js"></script>
|
||||
<script src="/script/auth-header.js"></script>
|
||||
@@ -149,23 +174,46 @@
|
||||
let pendingRemoveAvatar = false;
|
||||
/** @type {{ email: string, username: string, hasAvatar: boolean }} */
|
||||
let baseline = { email: '', username: '', hasAvatar: false };
|
||||
/** @type {number|null} */
|
||||
let prefsNoticeTimer = null;
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} fallback
|
||||
* @returns {string}
|
||||
*/
|
||||
function t(key, fallback) {
|
||||
return window.UaPageI18n?.t(key, fallback) || fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 프로필 토글 UI
|
||||
* @param {string} toggleId
|
||||
* @param {string} circleId
|
||||
* @param {string} checkboxId
|
||||
* @param {string|null} stateId
|
||||
* @param {boolean} on
|
||||
*/
|
||||
function setProfileToggle(toggleId, circleId, checkboxId, on) {
|
||||
function setProfileToggle(toggleId, circleId, checkboxId, stateId, on) {
|
||||
const toggle = document.getElementById(toggleId);
|
||||
const circle = document.getElementById(circleId);
|
||||
const checkbox = document.getElementById(checkboxId);
|
||||
const stateEl = stateId ? document.getElementById(stateId) : null;
|
||||
if (!toggle || !circle || !checkbox) return;
|
||||
checkbox.checked = on;
|
||||
toggle.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
toggle.classList.toggle('is-on', on);
|
||||
circle.classList.toggle('is-on', on);
|
||||
if (stateEl) {
|
||||
const onLabel = stateEl.dataset.i18nOn
|
||||
? t(stateEl.dataset.i18nOn, 'ON')
|
||||
: 'ON';
|
||||
const offLabel = stateEl.dataset.i18nOff
|
||||
? t(stateEl.dataset.i18nOff, 'OFF')
|
||||
: 'OFF';
|
||||
stateEl.textContent = on ? onLabel : offLabel;
|
||||
stateEl.classList.toggle('is-on', on);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,15 +222,35 @@
|
||||
*/
|
||||
function fillDeckPrefs(user) {
|
||||
const prefs = UaDeckPrefs.getPrefs(user);
|
||||
setProfileToggle('pref-public-toggle', 'pref-public-circle', 'pref-public-check', prefs.defaultPublic);
|
||||
setProfileToggle(
|
||||
'pref-public-toggle',
|
||||
'pref-public-circle',
|
||||
'pref-public-check',
|
||||
'pref-public-state',
|
||||
prefs.defaultPublic,
|
||||
);
|
||||
setProfileToggle(
|
||||
'pref-ask-toggle',
|
||||
'pref-ask-circle',
|
||||
'pref-ask-check',
|
||||
'pref-ask-state',
|
||||
!prefs.skipPrompt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 덱 저장 설정 적용 안내
|
||||
*/
|
||||
function showPrefsSavedNotice() {
|
||||
const el = document.getElementById('profile-prefs-notice');
|
||||
if (!el) return;
|
||||
el.textContent = t('profileDeckPrefsSaved', '덱 저장 설정이 적용되었습니다.');
|
||||
if (prefsNoticeTimer) clearTimeout(prefsNoticeTimer);
|
||||
prefsNoticeTimer = window.setTimeout(() => {
|
||||
el.textContent = '';
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 덱 저장 설정 저장
|
||||
*/
|
||||
@@ -198,6 +266,7 @@
|
||||
cached.skipDeckPublicPrompt = prefs.skipPrompt;
|
||||
UaDeckApi.saveAuth(UaDeckApi.getToken(), cached);
|
||||
}
|
||||
showPrefsSavedNotice();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,6 +344,10 @@
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (window.UaPageI18n?.applyPageI18n) {
|
||||
await window.UaPageI18n.applyPageI18n();
|
||||
}
|
||||
|
||||
if (!UaDeckApi.getToken()) {
|
||||
location.href = '/login.html?redirect=' + encodeURIComponent('/profile.html');
|
||||
return;
|
||||
@@ -306,6 +379,7 @@
|
||||
'pref-public-toggle',
|
||||
'pref-public-circle',
|
||||
'pref-public-check',
|
||||
'pref-public-state',
|
||||
!checkbox.checked,
|
||||
);
|
||||
await persistDeckPrefs();
|
||||
@@ -313,10 +387,25 @@
|
||||
|
||||
document.getElementById('pref-ask-toggle').addEventListener('click', async () => {
|
||||
const checkbox = document.getElementById('pref-ask-check');
|
||||
setProfileToggle('pref-ask-toggle', 'pref-ask-circle', 'pref-ask-check', !checkbox.checked);
|
||||
setProfileToggle(
|
||||
'pref-ask-toggle',
|
||||
'pref-ask-circle',
|
||||
'pref-ask-check',
|
||||
'pref-ask-state',
|
||||
!checkbox.checked,
|
||||
);
|
||||
await persistDeckPrefs();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.profile-toggle-text').forEach((label) => {
|
||||
label.addEventListener('click', (e) => {
|
||||
const forId = label.getAttribute('for');
|
||||
if (!forId) return;
|
||||
const btn = document.getElementById(forId);
|
||||
if (btn && e.target !== btn) btn.click();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('profile-avatar-btn').addEventListener('click', () => {
|
||||
document.getElementById('profile-avatar-file').click();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user