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
빠르게 기록하는 개인 가계부
-
홈 화면에 설치해서 앱처럼 쓰고, 로그인 상태를 유지할 수 있게 구성했습니다.
+
계정을 만들면 내 자산, 카테고리, 거래 내역을 따로 관리할 수 있습니다.
+
+
+
+
+
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 테스트를 추가한다.