feat(auth): 비밀번호 표시 토글을 SVG 아이콘으로 통일

- AuthPasswordVisibilityToggle 공통 컴포넌트 추가
- signin·signup(비밀번호·확인)에 적용, 접근성 field-name 구분
- v0.0.61 문서 반영

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 12:37:30 +09:00
parent fd55d8af08
commit 3f7f51ff86
8 changed files with 125 additions and 26 deletions

View File

@@ -0,0 +1,80 @@
<script setup>
/**
* 비밀번호 필드 표시/숨김 토글(Material 스타일 눈 아이콘 SVG)
*/
const props = defineProps({
/** 비밀번호를 평문으로 표시할 때 true */
modelValue: {
type: Boolean,
required: true
},
/**
* 스크린 리더용 필드 이름(예: 비밀번호 확인)
* @type {string}
*/
fieldName: {
type: String,
default: '비밀번호'
}
})
const emit = defineEmits(['update:modelValue'])
/**
* 접근성용 레이블 문자열
* @param {'show' | 'hide'} kind - 보기 또는 숨기기
* @returns {string}
*/
const labelFor = (kind) => {
if (kind === 'show') {
return `${props.fieldName} 보기`
}
return `${props.fieldName} 숨기기`
}
/**
* 표시 상태를 반전한다.
* @returns {void}
*/
const toggle = () => {
emit('update:modelValue', !props.modelValue)
}
</script>
<template>
<button
class="auth-password-visibility-toggle flex h-10 shrink-0 items-center justify-center px-2.5 text-[#9ba3af] outline-none transition-opacity hover:opacity-80 focus-visible:ring-2 focus-visible:ring-[#2f6feb]/50"
type="button"
:aria-label="modelValue ? labelFor('hide') : labelFor('show')"
:aria-pressed="modelValue"
@click="toggle"
>
<!-- 비밀번호 숨김 상태: 보기( 열림) -->
<svg
v-if="!modelValue"
class="auth-password-visibility-toggle__icon h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
<!-- 비밀번호 표시 상태: 숨기기( 가림) -->
<svg
v-else
class="auth-password-visibility-toggle__icon h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
fill="currentColor"
aria-hidden="true"
>
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 22 19.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>
</button>
</template>

View File

@@ -1,5 +1,11 @@
# 의사결정 이력
## 2026-05-11 v0.0.61
### 인증 폼 비밀번호 토글 아이콘화
보기/숨기기 텍스트는 좁은 모바일에서 시각적 잡음이 되고 다국어·아이콘 일관성도 떨어져, Material 스타일 단일 경로 SVG(눈 열림·가림)를 공통 컴포넌트로 두었다. `aria-label`은 필드명(`field-name`)을 받아 회원가입의 확인 필드와 구분한다.
## 2026-05-11 v0.0.60
### 홈 Featured 모바일 스크롤·화살표 상태

View File

@@ -27,6 +27,7 @@
| 파일 | 화면 위치 |
|------|-----------|
| components/auth/AuthPasswordVisibilityToggle.vue | 로그인·회원가입 비밀번호 표시/숨김 토글(SVG, `field-name`으로 접근성 레이블 구분) |
| components/site/SiteHeader.vue | 모든 공개 페이지 상단, 우측 사용자 아바타 드롭다운(Anonymous/Sign up/Sign in), `lg`~`xl` 헤더 여백·반응형 검색창 폭 |
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+``sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 하단 푸터 `px-4`/`sm:px-5` |
| components/site/RightSidebar.vue | 오른쪽 사이드바, `lg+`는 고정 열 높이·스티키, 모바일은 본문 아래 전체 너비, 하단 푸터 `pr-3` |
@@ -97,8 +98,8 @@
| pages/tags/[slug].vue | `/tag/:slug` 리다이렉트 |
| pages/tag/[slug].vue | 태그별 글 목록, 상단 태그 헤더 + 리스트형 게시물 카드 |
| pages/pages/[slug].vue | 고정 페이지 상세 |
| pages/signup.vue | 회원가입 3단계 화면(환영/입력/이메일 확인, 재전송) |
| pages/signin.vue | 로그인 화면(다크 폼) |
| pages/signup.vue | 회원가입 3단계(환영/입력/이메일 확인, 재전송), 2단계 비밀번호·확인에 각각 SVG 표시 토글 |
| pages/signin.vue | 로그인(다크 폼), 비밀번호 SVG 표시 토글 |
## 서버 API

View File

@@ -88,7 +88,7 @@
- 3단계는 인증 메일 발송 안내와 재전송 버튼(쿨다운)을 제공한다.
- 이메일 링크 확인 전에는 회원가입이 완료되지 않으며, 인증 완료 액션 이후 로그인 화면으로 이동한다.
- 로그인 화면은 동일한 다크 톤 폼 레이아웃을 사용한다.
- 로그인 비밀번호 입력은 보기/숨기기 토글을 제공한다.
- 로그인·회원가입(2단계) 비밀번호 입력은 `AuthPasswordVisibilityToggle` SVG(눈 열림/가림) 토글로 표시 여부를 바꾼다. 스크린 리더용 `aria-label`은 필드별 `field-name`으로 구분한다.
- 인증 화면 상태 메시지는 오류/안내를 분리해 `aria-live`로 노출한다.
- 회원가입 1단계의 타이틀/설명은 `GET /api/site-settings``title`, `description` 값을 우선 사용한다.

View File

@@ -1,5 +1,10 @@
# 업데이트 이력
## v0.0.61
- 로그인·회원가입 비밀번호 표시 토글을 `AuthPasswordVisibilityToggle`(Material 스타일 SVG 눈 아이콘)으로 통일, 텍스트 보기/숨기기 제거.
- 회원가입 비밀번호·비밀번호 확인 각각 독립 토글·포커스 링을 가진 입력 행으로 정리.
## v0.0.60
- 홈 Featured 가로 트랙에 `touch-pan-x`·`-webkit-overflow-scrolling:touch`·`overscroll-x-contain`을 두어 모바일에서 손가락으로 가로 슬라이드(스크롤·스냅)가 잘 먹도록 함.

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "0.0.60",
"version": "0.0.61",
"private": true,
"type": "module",
"imports": {

View File

@@ -53,7 +53,7 @@ const submitSignIn = async () => {
<template>
<section class="auth-signin min-h-screen bg-[#0a0b0d] text-[#f5f7fa]">
<div class="mx-auto flex min-h-screen w-full max-w-[1280px] items-center px-5 py-12 sm:px-10 lg:px-16">
<div class="w-full max-w-[430px] rounded-2xl border border-[#1a212a] bg-[#0d1116] p-5 sm:p-8">
<div class="w-full max-w-[430px] p-5 sm:p-8">
<p class="text-2xl font-semibold leading-tight">
로그인
</p>
@@ -77,17 +77,11 @@ const submitSignIn = async () => {
<div class="flex items-center rounded-[8px] border border-[#1a212a] transition-colors focus-within:border-[#2f6feb]">
<input
v-model="form.password"
class="h-10 w-full bg-transparent px-3 text-sm outline-none"
class="h-10 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none"
:type="showPassword ? 'text' : 'password'"
autocomplete="current-password"
>
<button
class="px-3 text-xs text-[#9ba3af] transition-opacity hover:opacity-80"
type="button"
@click="showPassword = !showPassword"
>
{{ showPassword ? '숨기기' : '보기' }}
</button>
<AuthPasswordVisibilityToggle v-model="showPassword" />
</div>
</div>

View File

@@ -29,6 +29,9 @@ const errors = reactive({
passwordConfirm: ''
})
const showSignupPassword = ref(false)
const showSignupPasswordConfirm = ref(false)
const canResend = computed(() => resendCooldown.value <= 0)
const welcomeTitle = computed(() => `Welcome to ${siteSettings.value?.title || 'AFFiNE'}`)
const welcomeDescription = computed(() => siteSettings.value?.description || 'Configure your Self Host AFFiNE with a few simple settings.')
@@ -165,7 +168,7 @@ onBeforeUnmount(() => {
<template>
<section class="auth-signup min-h-screen bg-[#0a0b0d] text-[#f5f7fa]">
<div class="mx-auto flex min-h-screen w-full max-w-[1280px] items-start px-5 py-12 sm:px-10 sm:py-16 lg:px-16 lg:py-24">
<div class="flex min-h-[calc(100vh-6rem)] w-full max-w-[430px] flex-col rounded-2xl border border-[#1a212a] bg-[#0d1116] p-5 sm:min-h-[calc(100vh-8rem)] sm:p-8 lg:min-h-[calc(100vh-12rem)]">
<div class="flex min-h-[calc(100vh-6rem)] w-full max-w-[430px] flex-col rounded-2xl p-5 sm:min-h-[calc(100vh-8rem)] sm:p-8 lg:min-h-[calc(100vh-12rem)]">
<div>
<template v-if="currentStep === 1">
<p class="text-[32px] font-semibold leading-tight sm:text-[40px]">
@@ -215,13 +218,18 @@ onBeforeUnmount(() => {
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">비밀번호</label>
<input
v-model="form.password"
class="h-10 w-full rounded-[8px] border bg-transparent px-3 text-sm outline-none transition-colors"
:class="errors.password ? 'border-[#b03b43]' : 'border-[#1a212a] focus:border-[#2f6feb]'"
type="password"
autocomplete="new-password"
<div
class="flex items-center rounded-[8px] border transition-colors focus-within:border-[#2f6feb]"
:class="errors.password ? 'border-[#b03b43]' : 'border-[#1a212a]'"
>
<input
v-model="form.password"
class="h-10 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none"
:type="showSignupPassword ? 'text' : 'password'"
autocomplete="new-password"
>
<AuthPasswordVisibilityToggle v-model="showSignupPassword" />
</div>
<p v-if="errors.password" class="text-xs text-[#e05d67]">
{{ errors.password }}
</p>
@@ -229,13 +237,18 @@ onBeforeUnmount(() => {
<div class="space-y-1.5">
<label class="text-xs text-[#d8dee6]">비밀번호 확인</label>
<input
v-model="form.passwordConfirm"
class="h-10 w-full rounded-[8px] border bg-transparent px-3 text-sm outline-none transition-colors"
:class="errors.passwordConfirm ? 'border-[#b03b43]' : 'border-[#1a212a] focus:border-[#2f6feb]'"
type="password"
autocomplete="new-password"
<div
class="flex items-center rounded-[8px] border transition-colors focus-within:border-[#2f6feb]"
:class="errors.passwordConfirm ? 'border-[#b03b43]' : 'border-[#1a212a]'"
>
<input
v-model="form.passwordConfirm"
class="h-10 min-w-0 flex-1 bg-transparent px-3 text-sm outline-none"
:type="showSignupPasswordConfirm ? 'text' : 'password'"
autocomplete="new-password"
>
<AuthPasswordVisibilityToggle v-model="showSignupPasswordConfirm" field-name="비밀번호 확인" />
</div>
<p v-if="errors.passwordConfirm" class="text-xs text-[#e05d67]">
{{ errors.passwordConfirm }}
</p>