설정 액션 정리
This commit is contained in:
1
frontend/src/assets/icons/key.svg
Normal file
1
frontend/src/assets/icons/key.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M223.5-423.5Q200-447 200-480t23.5-56.5Q247-560 280-560t56.5 23.5Q360-513 360-480t-23.5 56.5Q313-400 280-400t-56.5-23.5ZM280-240q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z"/></svg>
|
||||
|
After Width: | Height: | Size: 508 B |
1
frontend/src/assets/icons/logout.svg
Normal file
1
frontend/src/assets/icons/logout.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
1
frontend/src/assets/icons/open_in_new.svg
Normal file
1
frontend/src/assets/icons/open_in_new.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="ffffff"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"/></svg>
|
||||
|
After Width: | Height: | Size: 299 B |
@@ -3,10 +3,14 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { api } from '../lib/api'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import { displayInitialFrom } from '../lib/display'
|
||||
import { homePath, loginPath } from '../lib/paths'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import { useToast } from '../composables/useToast'
|
||||
import keyIcon from '../assets/icons/key.svg'
|
||||
import logoutIcon from '../assets/icons/logout.svg'
|
||||
import openInNewIcon from '../assets/icons/open_in_new.svg'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
@@ -63,7 +67,6 @@ const nicknameUpdatedAt = computed(() => Number(auth.user?.nicknameUpdatedAt ||
|
||||
const nicknameChangeAvailableAt = computed(() => Number(auth.user?.nicknameChangeAvailableAt || 0))
|
||||
const nicknameChangeIntervalMs = computed(() => Number(auth.user?.nicknameChangeIntervalMs || 0))
|
||||
const nicknameChangeIntervalLabel = computed(() => String(auth.user?.nicknameChangeIntervalLabel || '2주'))
|
||||
const hasPendingAvatarChange = computed(() => !!avatarFile.value || removeAvatar.value)
|
||||
const canChangeNicknameNow = computed(() => {
|
||||
if (nicknameChangeIntervalMs.value <= 0) return true
|
||||
if (!nicknameUpdatedAt.value) return true
|
||||
@@ -74,12 +77,6 @@ const nicknameCooldownText = computed(() => {
|
||||
if (!nicknameUpdatedAt.value || canChangeNicknameNow.value) return `닉네임은 ${nicknameChangeIntervalLabel.value}에 한 번만 변경할 수 있어요.`
|
||||
return `다음 변경 가능 시점: ${formatDateTime(nicknameChangeAvailableAt.value)}`
|
||||
})
|
||||
const profileImageSummary = computed(() => {
|
||||
if (avatarFile.value) return '새 프로필 이미지를 저장 대기 중이에요.'
|
||||
if (removeAvatar.value) return '현재 프로필 이미지를 삭제 대기 중이에요.'
|
||||
return avatarUrl.value ? '프로필 이미지가 설정되어 있어요.' : '아직 프로필 이미지가 없어요.'
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!auth.hydrated) await auth.refresh()
|
||||
if (!auth.user) {
|
||||
@@ -131,6 +128,7 @@ function onAvatarChange(e) {
|
||||
avatarFile.value = file
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = URL.createObjectURL(file)
|
||||
saveAvatarChanges()
|
||||
}
|
||||
|
||||
function clearProfileFieldErrors() {
|
||||
@@ -152,6 +150,7 @@ function clearAvatar() {
|
||||
previewUrl.value = ''
|
||||
}
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
saveAvatarChanges()
|
||||
}
|
||||
|
||||
async function saveProfile(nextNickname = nickname.value) {
|
||||
@@ -358,32 +357,32 @@ async function logout() {
|
||||
<div class="settingsSummaryList">
|
||||
<div class="settingsSummaryItem">
|
||||
<div class="settingsSummaryItem__label">닉네임</div>
|
||||
<div class="settingsSummaryItem__value">{{ nickname || '미설정' }}</div>
|
||||
<div class="settingsSummaryItem__valueRow">
|
||||
<div class="settingsSummaryItem__value">{{ nickname || '미설정' }}</div>
|
||||
<button
|
||||
class="settingsIconAction"
|
||||
type="button"
|
||||
:disabled="!canChangeNicknameNow"
|
||||
aria-label="닉네임 변경"
|
||||
title="닉네임 변경"
|
||||
@click="openNicknameModal"
|
||||
>
|
||||
<SvgIcon :src="openInNewIcon" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="settingsSummaryItem__meta">{{ nicknameCooldownText }}</div>
|
||||
<button class="settingsTextAction" type="button" :disabled="!canChangeNicknameNow" @click="openNicknameModal">
|
||||
{{ canChangeNicknameNow ? '닉네임 변경' : '2주 제한 중' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="settingsSummaryItem">
|
||||
<div class="settingsSummaryItem__label">이메일</div>
|
||||
<div class="settingsSummaryItem__value">{{ authEmail }}</div>
|
||||
<div class="settingsSummaryItem__meta">현재 로그인에 사용하는 계정 이메일입니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsSummaryItem">
|
||||
<div class="settingsSummaryItem__label">프로필 이미지</div>
|
||||
<div class="settingsSummaryItem__value">{{ profileImageSummary }}</div>
|
||||
<div class="settingsSummaryItem__meta">이미지를 선택한 뒤에만 저장 버튼이 활성화됩니다.</div>
|
||||
<div class="settingsSummaryItem__valueRow">
|
||||
<div class="settingsSummaryItem__value">{{ authEmail }}</div>
|
||||
<span class="settingsSummaryItem__status">읽기 전용</span>
|
||||
</div>
|
||||
<div class="settingsSummaryItem__meta">현재 로그인에 사용하는 계정 이메일입니다. 지금은 설정 화면에서 직접 변경하지 않습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settingsActionRow">
|
||||
<button v-if="hasPendingAvatarChange" class="btn btn--save" type="button" :disabled="saving" @click="saveAvatarChanges">
|
||||
{{ saving ? '저장 중...' : '프로필 이미지 저장' }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -396,7 +395,9 @@ async function logout() {
|
||||
<div class="settingsCompactRow__title">비밀번호</div>
|
||||
<div class="settingsCompactRow__desc">현재 비밀번호 확인 후 새 비밀번호로 변경합니다.</div>
|
||||
</div>
|
||||
<button class="settingsTextAction" type="button" @click="openPasswordModal">비밀번호 변경</button>
|
||||
<button class="settingsIconAction settingsIconAction--solid" type="button" aria-label="비밀번호 변경" title="비밀번호 변경" @click="openPasswordModal">
|
||||
<SvgIcon :src="keyIcon" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -415,7 +416,9 @@ async function logout() {
|
||||
<div class="settingsCompactRow__title">로그아웃</div>
|
||||
<div class="settingsCompactRow__desc">이 기기에서 현재 세션을 종료합니다.</div>
|
||||
</div>
|
||||
<button class="btn btn--ghost" type="button" @click="logout">로그아웃</button>
|
||||
<button class="settingsIconAction settingsIconAction--solid" type="button" aria-label="로그아웃" title="로그아웃" @click="logout">
|
||||
<SvgIcon :src="logoutIcon" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -681,12 +684,30 @@ async function logout() {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.settingsSummaryItem__valueRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settingsSummaryItem__meta {
|
||||
font-size: 13px;
|
||||
color: var(--theme-text-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.settingsSummaryItem__status {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-pill-bg);
|
||||
color: var(--theme-text-soft);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settingsActionRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -770,22 +791,6 @@ async function logout() {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settingsTextAction {
|
||||
width: fit-content;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--theme-accent-bg);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settingsTextAction:disabled {
|
||||
color: var(--theme-text-soft);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -816,6 +821,29 @@ async function logout() {
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.settingsIconAction {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--theme-border-strong);
|
||||
background: var(--theme-surface);
|
||||
color: var(--theme-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settingsIconAction:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.settingsIconAction--solid {
|
||||
background: var(--theme-pill-bg);
|
||||
}
|
||||
|
||||
.settingsModalOverlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user