diff --git a/docs/changelog.md b/docs/changelog.md
index 46dd69f..58af617 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -46,3 +46,5 @@
- 거래 목록의 카테고리 아이콘 자리를 카테고리별 아이콘/색상 설정과 연결했다.
- 거래 입력/수정의 카테고리 선택을 실제 선택 가능한 최종 카테고리 중심으로 정리했다.
- README에 개발, 빌드, Prisma 관련 주요 명령과 현재 검증 기준을 정리했다.
+- TODO에서 요구사항과 맞지 않던 이체 관련 작업 항목을 제거했다.
+- 설정의 계정 관리 화면에서 이름 수정과 비밀번호 변경 기능을 구현했다.
diff --git a/docs/project-guidelines.md b/docs/project-guidelines.md
index 3a250c9..7f29386 100644
--- a/docs/project-guidelines.md
+++ b/docs/project-guidelines.md
@@ -14,6 +14,7 @@
- PC: 사이드바 내비게이션과 넓은 콘텐츠 영역을 사용한다.
- 입력 화면은 한 화면에서 빠르게 끝나는 흐름을 유지한다.
- 거래 입력에서는 시간 입력을 요구하지 않고 날짜만 선택한다.
+- 거래 타입은 수입과 지출만 사용하고 이체 타입이나 이체 탭은 만들지 않는다.
- 날짜 선택은 모바일 친화적인 바텀 시트 캘린더를 사용한다.
- 금액 입력은 기본값 `0`을 채우지 않고 placeholder로만 안내한다.
- 셀렉트 박스보다 모바일 친화적인 바텀 시트를 우선한다.
diff --git a/pages/settings.vue b/pages/settings.vue
index 3c93478..1221acd 100644
--- a/pages/settings.vue
+++ b/pages/settings.vue
@@ -58,11 +58,43 @@
-
- 로그인 계정
- {{ session?.name }}
- {{ session?.email }}
-
+
+
+
로그아웃
@@ -341,6 +373,20 @@ const shareForm = reactive({
name: '',
days: 7
})
+const accountPending = ref(false)
+const passwordPending = ref(false)
+const accountMessage = ref('')
+const passwordMessage = ref('')
+const accountMessageType = ref<'ok' | 'error'>('ok')
+const passwordMessageType = ref<'ok' | 'error'>('ok')
+const accountForm = reactive({
+ name: session.value?.name || ''
+})
+const passwordForm = reactive({
+ currentPassword: '',
+ nextPassword: '',
+ confirmPassword: ''
+})
const categoryEditForm = reactive({
name: '',
icon: 'circle',
@@ -396,6 +442,11 @@ function open(nextView: SettingsView) {
view.value = nextView
selectedParentId.value = ''
selectedCategoryId.value = ''
+ if (nextView === 'account') {
+ accountForm.name = session.value?.name || ''
+ accountMessage.value = ''
+ passwordMessage.value = ''
+ }
}
function goBack() {
@@ -501,6 +552,50 @@ async function copyShareUrl(token: string) {
await navigator.clipboard?.writeText(shareUrl(token))
}
+async function saveAccount() {
+ accountPending.value = true
+ accountMessage.value = ''
+ try {
+ const updated = await $fetch<{ id: string; email: string; name: string }>('/api/auth/me', {
+ method: 'PATCH',
+ body: accountForm
+ })
+ session.value = updated
+ accountMessageType.value = 'ok'
+ accountMessage.value = '계정 정보를 저장했습니다.'
+ } catch {
+ accountMessageType.value = 'error'
+ accountMessage.value = '계정 정보를 저장하지 못했습니다.'
+ } finally {
+ accountPending.value = false
+ }
+}
+
+async function changePassword() {
+ passwordPending.value = true
+ passwordMessage.value = ''
+ try {
+ if (passwordForm.nextPassword !== passwordForm.confirmPassword) {
+ throw new Error('password mismatch')
+ }
+ await $fetch('/api/auth/password', {
+ method: 'POST',
+ body: {
+ currentPassword: passwordForm.currentPassword,
+ nextPassword: passwordForm.nextPassword
+ }
+ })
+ Object.assign(passwordForm, { currentPassword: '', nextPassword: '', confirmPassword: '' })
+ passwordMessageType.value = 'ok'
+ passwordMessage.value = '비밀번호를 변경했습니다.'
+ } catch {
+ passwordMessageType.value = 'error'
+ passwordMessage.value = '비밀번호를 확인해 주세요.'
+ } finally {
+ passwordPending.value = false
+ }
+}
+
function shareUrl(token: string) {
if (!import.meta.client) return `/share/${token}`
return `${window.location.origin}/share/${token}`
diff --git a/server/api/auth/me.patch.ts b/server/api/auth/me.patch.ts
new file mode 100644
index 0000000..9f6d8a7
--- /dev/null
+++ b/server/api/auth/me.patch.ts
@@ -0,0 +1,19 @@
+import { readBody, createError } from 'h3'
+import { prisma } from '../../utils/db'
+import { requireUser } from '../../utils/auth'
+
+export default defineEventHandler(async (event) => {
+ const user = await requireUser(event)
+ const body = await readBody<{ name?: string }>(event)
+ const name = body.name?.trim()
+
+ if (!name || name.length > 40) {
+ throw createError({ statusCode: 400, statusMessage: 'Invalid name' })
+ }
+
+ return prisma.user.update({
+ where: { id: user.id },
+ data: { name },
+ select: { id: true, email: true, name: true }
+ })
+})
diff --git a/server/api/auth/password.post.ts b/server/api/auth/password.post.ts
new file mode 100644
index 0000000..d913d3e
--- /dev/null
+++ b/server/api/auth/password.post.ts
@@ -0,0 +1,27 @@
+import { compare, hash } from 'bcryptjs'
+import { readBody, createError } from 'h3'
+import { prisma } from '../../utils/db'
+import { requireUser } from '../../utils/auth'
+
+export default defineEventHandler(async (event) => {
+ const sessionUser = await requireUser(event)
+ const body = await readBody<{ currentPassword?: string; nextPassword?: string }>(event)
+ const currentPassword = body.currentPassword || ''
+ const nextPassword = body.nextPassword || ''
+
+ if (nextPassword.length < 8) {
+ throw createError({ statusCode: 400, statusMessage: 'Password must be at least 8 characters' })
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: sessionUser.id } })
+ if (!user || !(await compare(currentPassword, user.passwordHash))) {
+ throw createError({ statusCode: 401, statusMessage: 'Current password is invalid' })
+ }
+
+ await prisma.user.update({
+ where: { id: user.id },
+ data: { passwordHash: await hash(nextPassword, 10) }
+ })
+
+ return { ok: true }
+})
diff --git a/todo.md b/todo.md
index a7be5df..77de4c3 100644
--- a/todo.md
+++ b/todo.md
@@ -16,7 +16,7 @@
## 3. 거래 입력/수정
-- 수입, 지출, 이체 탭의 동작과 저장 로직을 완성한다.
+- 수입과 지출 탭의 동작과 저장 로직을 더 안정화한다.
- 날짜 선택 바텀 시트의 오늘/이번 달 빠른 이동 기능을 추가한다.
- 자산 선택 후 카테고리 선택으로 자연스럽게 이어지는 입력 흐름을 만든다.
- 금액 입력 키패드 또는 숫자 입력 UX를 개선한다.
@@ -36,7 +36,6 @@
- 자산 상세에서 일별/월별/연별 보기 전환을 구현한다.
- 자산 추가/수정 화면을 별도 상세 화면 또는 바텀 시트로 정리한다.
- 자산 삭제 또는 숨김 처리 규칙을 정한다.
-- 이체 거래가 자산 잔액에 정확히 반영되도록 구현한다.
## 6. 통계
@@ -55,15 +54,12 @@
## 8. 데이터와 API
-- 거래 API에서 이체 타입을 지원한다.
- 사용자 설정 테이블을 추가한다.
- 거래 목록 조회 성능을 고려해 필요한 index를 점검한다.
- CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다.
## 9. 인증과 사용자 경험
-- 계정 정보 수정 화면을 구현한다.
-- 비밀번호 변경 기능을 구현한다.
- 회원 탈퇴 또는 데이터 삭제 정책을 정한다.
- 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 더 명확히 보여준다.
- 빈 데이터 상태 화면을 페이지별로 다듬는다.