feat(settings): 회원 썸네일을 미리보기 중심 UI로 개편
설정 화면에서 썸네일 URL 텍스트 노출을 제거하고 아바타 미리보기와 이미지 변경/제거 액션을 중심으로 재구성해 계정 정보 확인 흐름을 직관적으로 맞춘다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-05-11 v0.0.77
|
||||||
|
|
||||||
|
### 회원 설정 썸네일 표시 방식 전환
|
||||||
|
|
||||||
|
설정 화면에서 썸네일 URL 문자열을 직접 보여 주는 방식은 실제 사용자에게 의미가 낮고, 업로드 결과를 직관적으로 확인하기 어려웠다. 프로필 카드에서 아바타 미리보기와 이미지 변경/제거 액션을 바로 제공해 확인-수정 흐름을 단순화했다.
|
||||||
|
|
||||||
## 2026-05-11 v0.0.76
|
## 2026-05-11 v0.0.76
|
||||||
|
|
||||||
### 회원 썸네일 중앙 1:1 강제 크롭
|
### 회원 썸네일 중앙 1:1 강제 크롭
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
| pages/pages/[slug].vue | 고정 페이지 상세 |
|
| pages/pages/[slug].vue | 고정 페이지 상세 |
|
||||||
| pages/signup.vue | 회원가입 3단계, 2단계 입력에 `auth-form-input`, 패널 `auth-signup__panel`(보더·배경) |
|
| pages/signup.vue | 회원가입 3단계, 2단계 입력에 `auth-form-input`, 패널 `auth-signup__panel`(보더·배경) |
|
||||||
| pages/signin.vue | 로그인(다크 폼, `[color-scheme:dark]`, 입력 `auth-form-input`), 비밀번호 SVG 토글, 회원 로그인 API 연동 |
|
| pages/signin.vue | 로그인(다크 폼, `[color-scheme:dark]`, 입력 `auth-form-input`), 비밀번호 SVG 토글, 회원 로그인 API 연동 |
|
||||||
| pages/settings/index.vue | 회원 설정(썸네일 URL, 닉네임 변경/중복확인, 비밀번호 변경, 회원 탈퇴) |
|
| pages/settings/index.vue | 회원 설정(썸네일 미리보기/이미지 변경·제거, 닉네임 변경/중복확인, 비밀번호 변경, 회원 탈퇴) |
|
||||||
|
|
||||||
## 서버 API
|
## 서버 API
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
- `/tag/:slug` - 태그별 게시물 목록
|
- `/tag/:slug` - 태그별 게시물 목록
|
||||||
- `/signup` - 회원가입(3단계: 환영/입력/이메일 확인)
|
- `/signup` - 회원가입(3단계: 환영/입력/이메일 확인)
|
||||||
- `/signin` - 로그인
|
- `/signin` - 로그인
|
||||||
- `/settings` - 회원 설정(썸네일, 닉네임, 비밀번호, 회원 탈퇴)
|
- `/settings` - 회원 설정(썸네일 미리보기/변경/제거, 닉네임, 비밀번호, 회원 탈퇴)
|
||||||
- 기존 `/posts/:slug`, `/tags/:slug` 상세 경로는 새 단수형 상세 경로로 리다이렉트한다.
|
- 기존 `/posts/:slug`, `/tags/:slug` 상세 경로는 새 단수형 상세 경로로 리다이렉트한다.
|
||||||
|
|
||||||
### 공개 인증 화면(초기)
|
### 공개 인증 화면(초기)
|
||||||
@@ -589,6 +589,6 @@ APP_PORT=43118
|
|||||||
|
|
||||||
## 버전 관리
|
## 버전 관리
|
||||||
|
|
||||||
- 현재 버전: v0.0.76
|
- 현재 버전: v0.0.77
|
||||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
- [ ] Thred 참고 화면 기준 시각 QA
|
- [ ] Thred 참고 화면 기준 시각 QA
|
||||||
- [ ] 사이드바 토글 애니메이션 세부 조정
|
- [ ] 사이드바 토글 애니메이션 세부 조정
|
||||||
- [ ] 사용자 화면 테마 전환 초기 로드 깜빡임(FART) 최소화
|
- [ ] 사용자 화면 테마 전환 초기 로드 깜빡임(FART) 최소화
|
||||||
|
- [ ] 회원 설정 썸네일 수동 크롭 UI(선택 영역 지정) 도입 검토
|
||||||
|
|
||||||
## 콘텐츠 스타일 구현
|
## 콘텐츠 스타일 구현
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v0.0.77
|
||||||
|
|
||||||
|
- 회원 설정 프로필 영역에서 썸네일 URL 입력을 제거하고, 썸네일 미리보기 + 이미지 변경/제거 버튼 중심 UI로 개편.
|
||||||
|
- 썸네일 업로드 완료 안내 문구를 실제 동작과 맞게 간소화.
|
||||||
|
- 썸네일 수동 크롭 UI는 우선순위 낮은 후속 작업으로 `docs/todo.md`에 기록.
|
||||||
|
|
||||||
## v0.0.76
|
## v0.0.76
|
||||||
|
|
||||||
- 회원 썸네일 업로드 시 원본 비율과 관계없이 중앙 기준 1:1 정사각형으로 강제 크롭하도록 변경.
|
- 회원 썸네일 업로드 시 원본 비율과 관계없이 중앙 기준 1:1 정사각형으로 강제 크롭하도록 변경.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.76",
|
"version": "0.0.77",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const profileForm = reactive({
|
|||||||
avatarUrl: ''
|
avatarUrl: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const avatarInputRef = ref(null)
|
||||||
|
|
||||||
const passwordForm = reactive({
|
const passwordForm = reactive({
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
nextPassword: '',
|
nextPassword: '',
|
||||||
@@ -149,7 +151,7 @@ const uploadAvatar = async (event) => {
|
|||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
profileForm.avatarUrl = result.avatarUrl || ''
|
profileForm.avatarUrl = result.avatarUrl || ''
|
||||||
profileMessage.value = '썸네일이 업로드되었습니다. 프로필 저장을 눌러 반영하세요.'
|
profileMessage.value = '썸네일이 업로드되었습니다.'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
profileMessage.value = error?.data?.message || '썸네일 업로드에 실패했습니다.'
|
profileMessage.value = error?.data?.message || '썸네일 업로드에 실패했습니다.'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -160,6 +162,14 @@ const uploadAvatar = async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 썸네일 파일 선택창을 연다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const openAvatarFilePicker = () => {
|
||||||
|
avatarInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 비밀번호를 변경한다.
|
* 비밀번호를 변경한다.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -236,7 +246,54 @@ onMounted(loadProfile)
|
|||||||
<div v-else class="mt-5 flex flex-col gap-5">
|
<div v-else class="mt-5 flex flex-col gap-5">
|
||||||
<section class="rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-4">
|
<section class="rounded-[10px] border border-[var(--site-line)] bg-[var(--site-bg)] p-4">
|
||||||
<h2 class="text-sm font-semibold">프로필</h2>
|
<h2 class="text-sm font-semibold">프로필</h2>
|
||||||
<div class="mt-3 flex flex-col gap-3">
|
<div class="mt-3 flex flex-col gap-4">
|
||||||
|
<div class="settings-profile-avatar-card flex items-center gap-4 rounded-[12px] border border-[var(--site-line)] bg-[var(--site-panel)] p-3">
|
||||||
|
<div class="settings-profile-avatar-frame relative h-20 w-20 shrink-0 overflow-hidden rounded-full border border-[var(--site-line)] bg-[var(--site-bg)]">
|
||||||
|
<img
|
||||||
|
v-if="profileForm.avatarUrl"
|
||||||
|
:src="profileForm.avatarUrl"
|
||||||
|
alt="프로필 썸네일"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="grid h-full w-full place-items-center text-sm font-semibold site-muted"
|
||||||
|
>
|
||||||
|
{{ (profileForm.username || profileForm.email || '@').slice(0, 1).toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-xs site-muted">
|
||||||
|
프로필 이미지
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-[10px] border border-[var(--site-line)] px-3 py-1.5 text-xs transition-opacity hover:opacity-80 disabled:opacity-60"
|
||||||
|
:disabled="uploadingAvatar"
|
||||||
|
@click="openAvatarFilePicker"
|
||||||
|
>
|
||||||
|
{{ uploadingAvatar ? '업로드 중...' : '이미지 변경' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-[10px] border border-[var(--site-line)] px-3 py-1.5 text-xs site-muted transition-opacity hover:opacity-80 disabled:opacity-60"
|
||||||
|
:disabled="removingAvatar || !profileForm.avatarUrl"
|
||||||
|
@click="removeAvatar"
|
||||||
|
>
|
||||||
|
{{ removingAvatar ? '제거 중...' : '이미지 제거' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="avatarInputRef"
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||||
|
class="hidden"
|
||||||
|
:disabled="uploadingAvatar"
|
||||||
|
@change="uploadAvatar"
|
||||||
|
>
|
||||||
<label class="text-xs site-muted">이메일</label>
|
<label class="text-xs site-muted">이메일</label>
|
||||||
<input
|
<input
|
||||||
v-model="profileForm.email"
|
v-model="profileForm.email"
|
||||||
@@ -250,29 +307,6 @@ onMounted(loadProfile)
|
|||||||
type="text"
|
type="text"
|
||||||
class="h-10 rounded-[8px] border border-[var(--site-line)] bg-transparent px-3 text-sm outline-none focus-visible:border-[var(--site-accent)]"
|
class="h-10 rounded-[8px] border border-[var(--site-line)] bg-transparent px-3 text-sm outline-none focus-visible:border-[var(--site-accent)]"
|
||||||
>
|
>
|
||||||
<label class="text-xs site-muted">썸네일 URL</label>
|
|
||||||
<input
|
|
||||||
v-model="profileForm.avatarUrl"
|
|
||||||
type="text"
|
|
||||||
class="h-10 rounded-[8px] border border-[var(--site-line)] bg-transparent px-3 text-sm outline-none focus-visible:border-[var(--site-accent)]"
|
|
||||||
placeholder="https://..."
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="w-fit rounded-[10px] border border-[var(--site-line)] px-3 py-1.5 text-xs site-muted transition-opacity hover:opacity-80 disabled:opacity-60"
|
|
||||||
:disabled="removingAvatar"
|
|
||||||
@click="removeAvatar"
|
|
||||||
>
|
|
||||||
{{ removingAvatar ? '제거 중...' : '썸네일 제거' }}
|
|
||||||
</button>
|
|
||||||
<label class="text-xs site-muted">썸네일 업로드</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/jpeg,image/png,image/webp,image/gif"
|
|
||||||
class="block w-full text-xs site-muted file:mr-3 file:cursor-pointer file:rounded-[8px] file:border file:border-[var(--site-line)] file:bg-[var(--site-panel)] file:px-3 file:py-1.5"
|
|
||||||
:disabled="uploadingAvatar"
|
|
||||||
@change="uploadAvatar"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="site-accent-button mt-1 w-fit rounded-[10px] px-3 py-1.5 text-xs font-semibold disabled:opacity-60"
|
class="site-accent-button mt-1 w-fit rounded-[10px] px-3 py-1.5 text-xs font-semibold disabled:opacity-60"
|
||||||
|
|||||||
Reference in New Issue
Block a user