Add user preference settings
This commit is contained in:
1
assets/icons/add.svg
Normal file
1
assets/icons/add.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 182 B |
@@ -77,3 +77,5 @@
|
|||||||
- 자산 상세 화면의 월 이동 위치를 헤더 아래 공통 구조로 맞추고, 일별/월별/연별 보기 전환과 전환 모션을 추가했다.
|
- 자산 상세 화면의 월 이동 위치를 헤더 아래 공통 구조로 맞추고, 일별/월별/연별 보기 전환과 전환 모션을 추가했다.
|
||||||
- 로컬 개발 DB에 누락되어 있던 공유 링크와 카테고리 표시 migration을 적용하고, 빈 이체 migration 디렉터리를 제거했다.
|
- 로컬 개발 DB에 누락되어 있던 공유 링크와 카테고리 표시 migration을 적용하고, 빈 이체 migration 디렉터리를 제거했다.
|
||||||
- 설정 홈을 계정, 가계부, 데이터 섹션으로 나누어 각 항목의 목적이 먼저 보이도록 정리했다.
|
- 설정 홈을 계정, 가계부, 데이터 섹션으로 나누어 각 항목의 목적이 먼저 보이도록 정리했다.
|
||||||
|
- 플로팅 추가 버튼과 설정 화면 추가 버튼을 텍스트 `+` 대신 `assets/icons/add.svg` 아이콘으로 교체했다.
|
||||||
|
- 사용자 설정 테이블과 API를 추가하고, 설정 화면에서 주 화폐, 월 시작일, 주간 시작일을 저장할 수 있도록 했다.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
- 자산 관리는 활성/비활성 토글을 쓰지 않고, 등록된 자산은 모두 사용하는 것으로 본다.
|
- 자산 관리는 활성/비활성 토글을 쓰지 않고, 등록된 자산은 모두 사용하는 것으로 본다.
|
||||||
- 통계 화면은 그래프보다 숫자와 분류별 리스트를 우선한다.
|
- 통계 화면은 그래프보다 숫자와 분류별 리스트를 우선한다.
|
||||||
- 방향 이동 UI는 `assets/icons`의 키보드 화살표 SVG를 사용한다.
|
- 방향 이동 UI는 `assets/icons`의 키보드 화살표 SVG를 사용한다.
|
||||||
|
- 추가 버튼 UI는 텍스트 `+` 대신 `assets/icons/add.svg`를 사용한다.
|
||||||
- 거래 목록의 카테고리 표시는 하위 카테고리가 있으면 하위 이름만, 없으면 상위 이름만 보여준다.
|
- 거래 목록의 카테고리 표시는 하위 카테고리가 있으면 하위 이름만, 없으면 상위 이름만 보여준다.
|
||||||
- 카테고리 아이콘과 색상은 카테고리 관리 상세 화면에서 수정하고 거래 목록 아이콘에 즉시 반영한다.
|
- 카테고리 아이콘과 색상은 카테고리 관리 상세 화면에서 수정하고 거래 목록 아이콘에 즉시 반영한다.
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
- 자산은 활성/비활성으로 관리하지 않는다. 삭제는 거래가 연결되지 않은 자산에 한해 제공한다.
|
- 자산은 활성/비활성으로 관리하지 않는다. 삭제는 거래가 연결되지 않은 자산에 한해 제공한다.
|
||||||
- 거래에 연결되는 카테고리 선택은 하위가 있으면 하위 카테고리를 우선하고, 하위가 없는 경우에만 상위 카테고리를 직접 선택한다.
|
- 거래에 연결되는 카테고리 선택은 하위가 있으면 하위 카테고리를 우선하고, 하위가 없는 경우에만 상위 카테고리를 직접 선택한다.
|
||||||
- CSV Export는 데이터 보존을 위한 필수 기능으로 유지한다.
|
- CSV Export는 데이터 보존을 위한 필수 기능으로 유지한다.
|
||||||
|
- 주 화폐, 월 시작일, 주간 시작일 같은 사용자별 기본 설정은 `UserSetting`에 저장한다.
|
||||||
|
|
||||||
## 개발/운영 기준
|
## 개발/운영 기준
|
||||||
|
|
||||||
|
|||||||
@@ -59,15 +59,16 @@
|
|||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-if="showFab"
|
v-if="showFab"
|
||||||
to="/add"
|
to="/add"
|
||||||
class="fixed bottom-20 right-4 z-30 flex h-14 w-14 items-center justify-center rounded-full bg-red-500 text-3xl leading-none text-white shadow-soft active:scale-95 md:bottom-10 md:right-10"
|
class="fixed bottom-20 right-4 z-30 flex h-14 w-14 items-center justify-center rounded-full bg-red-500 text-white shadow-soft active:scale-95 md:bottom-10 md:right-10"
|
||||||
aria-label="거래 추가"
|
aria-label="거래 추가"
|
||||||
>
|
>
|
||||||
+
|
<span class="nav-icon h-7 w-7" v-html="addIcon" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import addIconRaw from '~/assets/icons/add.svg?raw'
|
||||||
import assetIconRaw from '~/assets/icons/account_balance_wallet.svg?raw'
|
import assetIconRaw from '~/assets/icons/account_balance_wallet.svg?raw'
|
||||||
import reportIconRaw from '~/assets/icons/bar_chart.svg?raw'
|
import reportIconRaw from '~/assets/icons/bar_chart.svg?raw'
|
||||||
import receiptIconRaw from '~/assets/icons/receipt_long.svg?raw'
|
import receiptIconRaw from '~/assets/icons/receipt_long.svg?raw'
|
||||||
@@ -85,6 +86,7 @@ const items = [
|
|||||||
{ to: '/assets', label: '자산', icon: navIcon(assetIconRaw) },
|
{ to: '/assets', label: '자산', icon: navIcon(assetIconRaw) },
|
||||||
{ to: '/settings', label: '설정', icon: navIcon(settingsIconRaw) }
|
{ to: '/settings', label: '설정', icon: navIcon(settingsIconRaw) }
|
||||||
]
|
]
|
||||||
|
const addIcon = navIcon(addIconRaw)
|
||||||
|
|
||||||
const showNav = computed(() => route.path !== '/login')
|
const showNav = computed(() => route.path !== '/login')
|
||||||
const showFab = computed(() => showNav.value && route.path !== '/add' && !route.path.startsWith('/transactions/'))
|
const showFab = computed(() => showNav.value && route.path !== '/add' && !route.path.startsWith('/transactions/'))
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<button
|
<button
|
||||||
v-if="showAddButton"
|
v-if="showAddButton"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-9 w-9 items-center justify-center rounded-md text-2xl leading-none text-brand-700 active:bg-paper"
|
class="flex h-9 w-9 items-center justify-center rounded-md text-brand-700 active:bg-paper"
|
||||||
aria-label="추가"
|
aria-label="추가"
|
||||||
@click="openAddSheet"
|
@click="openAddSheet"
|
||||||
>
|
>
|
||||||
+
|
<span class="nav-icon h-6 w-6" v-html="addIcon" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -161,6 +161,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="view === 'preferences'" class="px-4 py-4">
|
||||||
|
<form class="space-y-5 border-y border-line bg-white px-4 py-4 md:rounded-md md:border" @submit.prevent="savePreferences">
|
||||||
|
<div>
|
||||||
|
<span class="label">주 화폐</span>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="currency in currencyOptions"
|
||||||
|
:key="currency.value"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border px-3 py-2.5 text-sm font-bold"
|
||||||
|
:class="preferenceForm.primaryCurrency === currency.value ? 'border-red-500 bg-red-50 text-red-600' : 'border-line text-muted'"
|
||||||
|
@click="preferenceForm.primaryCurrency = currency.value"
|
||||||
|
>
|
||||||
|
{{ currency.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="label" for="monthStartDay">월 시작일</label>
|
||||||
|
<select id="monthStartDay" v-model.number="preferenceForm.monthStartDay" class="field">
|
||||||
|
<option v-for="day in monthStartOptions" :key="day" :value="day">{{ day }}일</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="label">주간 시작일</span>
|
||||||
|
<div class="grid grid-cols-4 gap-2 sm:grid-cols-7">
|
||||||
|
<button
|
||||||
|
v-for="day in weekStartOptions"
|
||||||
|
:key="day.value"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md border px-2 py-2.5 text-xs font-bold"
|
||||||
|
:class="preferenceForm.weekStartDay === day.value ? 'border-red-500 bg-red-50 text-red-600' : 'border-line text-muted'"
|
||||||
|
@click="preferenceForm.weekStartDay = day.value"
|
||||||
|
>
|
||||||
|
{{ day.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="preferenceMessage" class="text-sm font-semibold" :class="preferenceMessageType === 'error' ? 'text-red-600' : 'text-brand-700'">
|
||||||
|
{{ preferenceMessage }}
|
||||||
|
</p>
|
||||||
|
<button class="btn-primary w-full" type="submit" :disabled="preferencePending">
|
||||||
|
{{ preferencePending ? '저장 중...' : '기본 설정 저장' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="view === 'assets'" class="py-4">
|
<div v-else-if="view === 'assets'" class="py-4">
|
||||||
<form v-if="selectedAsset" class="px-4" @submit.prevent="saveAsset">
|
<form v-if="selectedAsset" class="px-4" @submit.prevent="saveAsset">
|
||||||
<section class="space-y-5 border-y border-line bg-white px-4 py-4">
|
<section class="space-y-5 border-y border-line bg-white px-4 py-4">
|
||||||
@@ -410,11 +460,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
import addIconRaw from '~/assets/icons/add.svg?raw'
|
||||||
import { CATEGORY_COLOR_OPTIONS, CATEGORY_ICON_OPTIONS, categoryColor, categoryGlyph } from '~/utils/categoryPresentation'
|
import { CATEGORY_COLOR_OPTIONS, CATEGORY_ICON_OPTIONS, categoryColor, categoryGlyph } from '~/utils/categoryPresentation'
|
||||||
|
|
||||||
type SettingsView = 'home' | 'account' | 'sharing' | 'assets' | 'categories'
|
type SettingsView = 'home' | 'account' | 'sharing' | 'preferences' | 'assets' | 'categories'
|
||||||
type CategoryType = 'income' | 'expense'
|
type CategoryType = 'income' | 'expense'
|
||||||
|
|
||||||
|
const addIcon = addIconRaw
|
||||||
|
.replace(/fill="[^"]*"/g, 'fill="currentColor"')
|
||||||
|
.replace(/height="[^"]*"/, 'height="100%"')
|
||||||
|
.replace(/width="[^"]*"/, 'width="100%"')
|
||||||
const view = ref<SettingsView>('home')
|
const view = ref<SettingsView>('home')
|
||||||
const session = useState<{ id: string; email: string; name: string } | null>('session')
|
const session = useState<{ id: string; email: string; name: string } | null>('session')
|
||||||
const categoryType = ref<CategoryType>('expense')
|
const categoryType = ref<CategoryType>('expense')
|
||||||
@@ -427,6 +482,7 @@ const categoryName = ref('')
|
|||||||
const { data: assetData, refresh: refreshAssets } = await useFetch('/api/assets')
|
const { data: assetData, refresh: refreshAssets } = await useFetch('/api/assets')
|
||||||
const { data: categoryData, refresh: refreshCategories } = await useFetch('/api/categories')
|
const { data: categoryData, refresh: refreshCategories } = await useFetch('/api/categories')
|
||||||
const { data: shareLinkData, refresh: refreshShareLinks } = await useFetch('/api/share-links')
|
const { data: shareLinkData, refresh: refreshShareLinks } = await useFetch('/api/share-links')
|
||||||
|
const { data: preferenceData, refresh: refreshPreferences } = await useFetch('/api/user-settings')
|
||||||
|
|
||||||
const menuSections = [
|
const menuSections = [
|
||||||
{
|
{
|
||||||
@@ -443,6 +499,7 @@ const menuSections = [
|
|||||||
title: '가계부',
|
title: '가계부',
|
||||||
description: '입력할 때 사용하는 기본 항목입니다.',
|
description: '입력할 때 사용하는 기본 항목입니다.',
|
||||||
items: [
|
items: [
|
||||||
|
{ id: 'preferences', label: '기본 설정', description: '화폐, 월 시작일, 주간 시작일' },
|
||||||
{ id: 'assets', label: '자산 관리', description: '자산 추가, 수정, 삭제' },
|
{ id: 'assets', label: '자산 관리', description: '자산 추가, 수정, 삭제' },
|
||||||
{ id: 'categories', label: '카테고리 관리', description: '수입/지출 상위 분류와 하위 분류' }
|
{ id: 'categories', label: '카테고리 관리', description: '수입/지출 상위 분류와 하위 분류' }
|
||||||
]
|
]
|
||||||
@@ -467,10 +524,13 @@ const shareForm = reactive({
|
|||||||
})
|
})
|
||||||
const accountPending = ref(false)
|
const accountPending = ref(false)
|
||||||
const passwordPending = ref(false)
|
const passwordPending = ref(false)
|
||||||
|
const preferencePending = ref(false)
|
||||||
const accountMessage = ref('')
|
const accountMessage = ref('')
|
||||||
const passwordMessage = ref('')
|
const passwordMessage = ref('')
|
||||||
|
const preferenceMessage = ref('')
|
||||||
const accountMessageType = ref<'ok' | 'error'>('ok')
|
const accountMessageType = ref<'ok' | 'error'>('ok')
|
||||||
const passwordMessageType = ref<'ok' | 'error'>('ok')
|
const passwordMessageType = ref<'ok' | 'error'>('ok')
|
||||||
|
const preferenceMessageType = ref<'ok' | 'error'>('ok')
|
||||||
const accountForm = reactive({
|
const accountForm = reactive({
|
||||||
name: session.value?.name || ''
|
name: session.value?.name || ''
|
||||||
})
|
})
|
||||||
@@ -479,6 +539,18 @@ const passwordForm = reactive({
|
|||||||
nextPassword: '',
|
nextPassword: '',
|
||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
})
|
})
|
||||||
|
const preferenceForm = reactive({
|
||||||
|
primaryCurrency: 'KRW',
|
||||||
|
monthStartDay: 1,
|
||||||
|
weekStartDay: 0
|
||||||
|
})
|
||||||
|
const currencyOptions = [
|
||||||
|
{ value: 'KRW', label: 'KRW' },
|
||||||
|
{ value: 'USD', label: 'USD' },
|
||||||
|
{ value: 'JPY', label: 'JPY' }
|
||||||
|
]
|
||||||
|
const monthStartOptions = Array.from({ length: 28 }, (_, index) => index + 1)
|
||||||
|
const weekStartOptions = ['일', '월', '화', '수', '목', '금', '토'].map((label, value) => ({ label, value }))
|
||||||
const categoryEditForm = reactive({
|
const categoryEditForm = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
icon: 'circle',
|
icon: 'circle',
|
||||||
@@ -491,6 +563,7 @@ const categoryColorOptions = CATEGORY_COLOR_OPTIONS
|
|||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (view.value === 'account') return '계정 관리'
|
if (view.value === 'account') return '계정 관리'
|
||||||
if (view.value === 'sharing') return '공유 링크 관리'
|
if (view.value === 'sharing') return '공유 링크 관리'
|
||||||
|
if (view.value === 'preferences') return '기본 설정'
|
||||||
if (selectedAsset.value) return '자산 정보'
|
if (selectedAsset.value) return '자산 정보'
|
||||||
if (view.value === 'assets') return '자산 관리'
|
if (view.value === 'assets') return '자산 관리'
|
||||||
if (selectedCategory.value) return '카테고리 정보'
|
if (selectedCategory.value) return '카테고리 정보'
|
||||||
@@ -547,6 +620,15 @@ watch(selectedAsset, (asset: any) => {
|
|||||||
assetMessage.value = ''
|
assetMessage.value = ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(preferenceData, (value: any) => {
|
||||||
|
if (!value) return
|
||||||
|
Object.assign(preferenceForm, {
|
||||||
|
primaryCurrency: value.primaryCurrency || 'KRW',
|
||||||
|
monthStartDay: value.monthStartDay || 1,
|
||||||
|
weekStartDay: value.weekStartDay ?? 0
|
||||||
|
})
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
function open(nextView: SettingsView) {
|
function open(nextView: SettingsView) {
|
||||||
view.value = nextView
|
view.value = nextView
|
||||||
selectedParentId.value = ''
|
selectedParentId.value = ''
|
||||||
@@ -559,6 +641,9 @@ function open(nextView: SettingsView) {
|
|||||||
accountMessage.value = ''
|
accountMessage.value = ''
|
||||||
passwordMessage.value = ''
|
passwordMessage.value = ''
|
||||||
}
|
}
|
||||||
|
if (nextView === 'preferences') {
|
||||||
|
preferenceMessage.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
@@ -792,6 +877,25 @@ async function changePassword() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePreferences() {
|
||||||
|
preferencePending.value = true
|
||||||
|
preferenceMessage.value = ''
|
||||||
|
try {
|
||||||
|
await $fetch('/api/user-settings', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: preferenceForm
|
||||||
|
})
|
||||||
|
await refreshPreferences()
|
||||||
|
preferenceMessageType.value = 'ok'
|
||||||
|
preferenceMessage.value = '기본 설정을 저장했습니다.'
|
||||||
|
} catch {
|
||||||
|
preferenceMessageType.value = 'error'
|
||||||
|
preferenceMessage.value = '기본 설정을 저장하지 못했습니다.'
|
||||||
|
} finally {
|
||||||
|
preferencePending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function shareUrl(token: string) {
|
function shareUrl(token: string) {
|
||||||
if (!import.meta.client) return `/share/${token}`
|
if (!import.meta.client) return `/share/${token}`
|
||||||
return `${window.location.origin}/share/${token}`
|
return `${window.location.origin}/share/${token}`
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserSetting" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"primaryCurrency" TEXT NOT NULL DEFAULT 'KRW',
|
||||||
|
"monthStartDay" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"weekStartDay" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserSetting_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserSetting_userId_key" ON "UserSetting"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserSetting" ADD CONSTRAINT "UserSetting_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -38,6 +38,19 @@ model User {
|
|||||||
transactions Transaction[]
|
transactions Transaction[]
|
||||||
recurringTransactions RecurringTransaction[]
|
recurringTransactions RecurringTransaction[]
|
||||||
shareLinks ShareLink[]
|
shareLinks ShareLink[]
|
||||||
|
setting UserSetting?
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserSetting {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
primaryCurrency String @default("KRW")
|
||||||
|
monthStartDay Int @default(1)
|
||||||
|
weekStartDay Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Asset {
|
model Asset {
|
||||||
|
|||||||
11
server/api/user-settings/index.get.ts
Normal file
11
server/api/user-settings/index.get.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { prisma } from '../../utils/db'
|
||||||
|
import { requireUser } from '../../utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event)
|
||||||
|
return prisma.userSetting.upsert({
|
||||||
|
where: { userId: user.id },
|
||||||
|
update: {},
|
||||||
|
create: { userId: user.id }
|
||||||
|
})
|
||||||
|
})
|
||||||
34
server/api/user-settings/index.patch.ts
Normal file
34
server/api/user-settings/index.patch.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { createError, readBody } from 'h3'
|
||||||
|
import { prisma } from '../../utils/db'
|
||||||
|
import { requireUser } from '../../utils/auth'
|
||||||
|
|
||||||
|
const currencies = new Set(['KRW', 'USD', 'JPY'])
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const user = await requireUser(event)
|
||||||
|
const body = await readBody<{
|
||||||
|
primaryCurrency?: string
|
||||||
|
monthStartDay?: number
|
||||||
|
weekStartDay?: number
|
||||||
|
}>(event)
|
||||||
|
|
||||||
|
const primaryCurrency = body.primaryCurrency || 'KRW'
|
||||||
|
const monthStartDay = Number(body.monthStartDay || 1)
|
||||||
|
const weekStartDay = Number(body.weekStartDay ?? 0)
|
||||||
|
|
||||||
|
if (!currencies.has(primaryCurrency)) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid currency' })
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(monthStartDay) || monthStartDay < 1 || monthStartDay > 28) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid month start day' })
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(weekStartDay) || weekStartDay < 0 || weekStartDay > 6) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Invalid week start day' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return prisma.userSetting.upsert({
|
||||||
|
where: { userId: user.id },
|
||||||
|
update: { primaryCurrency, monthStartDay, weekStartDay },
|
||||||
|
create: { userId: user.id, primaryCurrency, monthStartDay, weekStartDay }
|
||||||
|
})
|
||||||
|
})
|
||||||
2
todo.md
2
todo.md
@@ -30,12 +30,10 @@
|
|||||||
|
|
||||||
## 6. 설정
|
## 6. 설정
|
||||||
|
|
||||||
- 주 화폐, 월 시작일, 주간 시작일 같은 기본 설정 항목을 구현한다.
|
|
||||||
- 설정 변경이 실제 화면 계산과 표시 방식에 반영되도록 연결한다.
|
- 설정 변경이 실제 화면 계산과 표시 방식에 반영되도록 연결한다.
|
||||||
|
|
||||||
## 7. 데이터와 API
|
## 7. 데이터와 API
|
||||||
|
|
||||||
- 사용자 설정 테이블을 추가한다.
|
|
||||||
- 거래 목록 조회 성능을 고려해 필요한 index를 점검한다.
|
- 거래 목록 조회 성능을 고려해 필요한 index를 점검한다.
|
||||||
- CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다.
|
- CSV 내보내기 컬럼을 실제 사용 흐름에 맞게 정리한다.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user