diff --git a/docs/changelog.md b/docs/changelog.md index c377b42..76a529e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -34,3 +34,6 @@ - 거래 목록 API에 검색어, 유형, 자산, 카테고리 필터 query를 추가했다. - 월 선택 바텀 시트를 공통 컴포넌트로 분리하고 내역, 통계, 자산 화면에 적용했다. - 거래 추가 화면을 거래 수정 화면과 같은 행 기반 입력 구조로 정리했다. +- 회원가입 API와 로그인 화면의 로그인/회원가입 전환 UI를 추가했다. +- 가입 시 기본 자산과 기본 수입/지출 카테고리가 생성되도록 했다. +- 읽기 전용 공유 링크 기능 계획을 TODO에 추가했다. diff --git a/pages/login.vue b/pages/login.vue index 57bbfd1..088daca 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -3,29 +3,64 @@

Money

빠르게 기록하는 개인 가계부

-

홈 화면에 설치해서 앱처럼 쓰고, 로그인 상태를 유지할 수 있게 구성했습니다.

+

계정을 만들면 내 자산, 카테고리, 거래 내역을 따로 관리할 수 있습니다.

+
+ +
+ +
+
+ + +
- +
- +

{{ error }}

diff --git a/prisma/seed.ts b/prisma/seed.ts index ecd4e85..7ef154c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,5 +1,6 @@ import { hash } from 'bcryptjs' import { PrismaClient } from '@prisma/client' +import { createDefaultMoneyData } from '../server/utils/defaults' const prisma = new PrismaClient() @@ -14,57 +15,17 @@ async function main() { } }) - const cash = await prisma.asset.upsert({ - where: { id: 'seed-cash' }, - update: {}, - create: { - id: 'seed-cash', - userId: user.id, - name: '현금', - balanceType: 'cumulative', - initialBalance: 0, - sortOrder: 0 - } - }) - - await prisma.asset.upsert({ - where: { id: 'seed-living' }, - update: {}, - create: { - id: 'seed-living', - userId: user.id, - name: '생활비', - balanceType: 'monthly_reset', - sortOrder: 1 - } - }) - - const incomeParent = await prisma.category.upsert({ - where: { id: 'seed-income-work' }, - update: {}, - create: { id: 'seed-income-work', userId: user.id, type: 'income', name: '근로', sortOrder: 0 } - }) - - const salary = await prisma.category.upsert({ - where: { id: 'seed-income-salary' }, - update: {}, - create: { id: 'seed-income-salary', userId: user.id, type: 'income', name: '월급', parentId: incomeParent.id, sortOrder: 0 } - }) - - const foodParent = await prisma.category.upsert({ - where: { id: 'seed-expense-food' }, - update: {}, - create: { id: 'seed-expense-food', userId: user.id, type: 'expense', name: '식비', sortOrder: 0 } - }) - - const meal = await prisma.category.upsert({ - where: { id: 'seed-expense-meal' }, - update: {}, - create: { id: 'seed-expense-meal', userId: user.id, type: 'expense', name: '외식', parentId: foodParent.id, sortOrder: 0 } - }) + const assetCount = await prisma.asset.count({ where: { userId: user.id } }) + const categoryCount = await prisma.category.count({ where: { userId: user.id } }) + if (assetCount === 0 && categoryCount === 0) { + await createDefaultMoneyData(prisma, user.id) + } const transactionCount = await prisma.transaction.count({ where: { userId: user.id } }) if (transactionCount === 0) { + const cash = await prisma.asset.findFirstOrThrow({ where: { userId: user.id }, orderBy: { sortOrder: 'asc' } }) + const salary = await prisma.category.findFirstOrThrow({ where: { userId: user.id, type: 'income', name: '월급' } }) + const meal = await prisma.category.findFirstOrThrow({ where: { userId: user.id, type: 'expense', name: '외식' } }) await prisma.transaction.createMany({ data: [ { diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts new file mode 100644 index 0000000..25fcf73 --- /dev/null +++ b/server/api/auth/register.post.ts @@ -0,0 +1,34 @@ +import { hash } from 'bcryptjs' +import { readBody, createError } from 'h3' +import { prisma } from '../../utils/db' +import { setSession } from '../../utils/auth' +import { createDefaultMoneyData } from '../../utils/defaults' + +export default defineEventHandler(async (event) => { + const body = await readBody<{ email: string; password: string; name?: string }>(event) + const email = body.email?.trim().toLowerCase() + const password = body.password || '' + const name = body.name?.trim() || email?.split('@')[0] || '사용자' + + if (!email || !email.includes('@') || password.length < 8) { + throw createError({ statusCode: 400, statusMessage: 'Invalid registration' }) + } + + const exists = await prisma.user.findUnique({ where: { email } }) + if (exists) { + throw createError({ statusCode: 409, statusMessage: 'Email already exists' }) + } + + const user = await prisma.user.create({ + data: { + email, + name, + passwordHash: await hash(password, 10) + } + }) + + await createDefaultMoneyData(prisma, user.id) + setSession(event, user.id) + + return { id: user.id, email: user.email, name: user.name } +}) diff --git a/server/utils/defaults.ts b/server/utils/defaults.ts new file mode 100644 index 0000000..de5798c --- /dev/null +++ b/server/utils/defaults.ts @@ -0,0 +1,36 @@ +import type { PrismaClient } from '@prisma/client' + +export async function createDefaultMoneyData(prisma: PrismaClient, userId: string) { + const cash = await prisma.asset.create({ + data: { + userId, + name: '기본자산', + balanceType: 'cumulative', + initialBalance: 0, + sortOrder: 0 + } + }) + + const incomeParent = await prisma.category.create({ + data: { userId, type: 'income', name: '근로', sortOrder: 0 } + }) + await prisma.category.create({ + data: { userId, type: 'income', name: '월급', parentId: incomeParent.id, sortOrder: 0 } + }) + + const foodParent = await prisma.category.create({ + data: { userId, type: 'expense', name: '식비', sortOrder: 0 } + }) + await prisma.category.create({ + data: { userId, type: 'expense', name: '외식', parentId: foodParent.id, sortOrder: 0 } + }) + + const livingParent = await prisma.category.create({ + data: { userId, type: 'expense', name: '생활', sortOrder: 1 } + }) + await prisma.category.create({ + data: { userId, type: 'expense', name: '마트', parentId: livingParent.id, sortOrder: 0 } + }) + + return { cash } +} diff --git a/todo.md b/todo.md index e04972b..9521f80 100644 --- a/todo.md +++ b/todo.md @@ -63,17 +63,30 @@ - 검색과 필터용 API query를 추가한다. - 카테고리 아이콘/색상 필드를 Prisma schema에 추가한다. - 사용자 설정 테이블을 추가한다. +- 읽기 전용 공유 링크 테이블과 만료 기간 필드를 추가한다. - 거래 목록 조회 성능을 고려해 필요한 index를 점검한다. - CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다. ## 9. 인증과 사용자 경험 -- 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 명확히 보여준다. +- 계정 정보 수정 화면을 구현한다. +- 비밀번호 변경 기능을 구현한다. +- 회원 탈퇴 또는 데이터 삭제 정책을 정한다. +- 로그인 실패, 세션 만료, 네트워크 오류 상태를 화면에서 더 명확히 보여준다. - 로그아웃 동작과 위치를 정리한다. - 첫 실행 시 기본 자산/카테고리 생성 흐름을 점검한다. - 빈 데이터 상태 화면을 페이지별로 다듬는다. -## 10. 품질 +## 10. 공유 + +- 사용자가 공유 링크를 생성하는 화면을 구현한다. +- 공유 링크의 조회 가능 기간을 설정할 수 있게 한다. +- 공유 링크 접근자는 읽기 전용으로만 내역, 통계, 자산을 볼 수 있게 한다. +- 공유 링크 만료, 삭제, 재생성 기능을 구현한다. +- 공유 화면에서 편집, 삭제, 내보내기 같은 쓰기 기능을 숨긴다. +- 공유 링크 접근 로그 또는 마지막 조회 시각을 기록할지 결정한다. + +## 11. 품질 - 핵심 유틸 함수 테스트를 추가한다. - 거래 생성, 수정, 삭제 API 테스트를 추가한다.