diff --git a/assets/css/main.css b/assets/css/main.css index eff57b1..8ebb5c5 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -69,4 +69,10 @@ button { .transaction-form-select { @apply flex min-h-10 w-full items-center justify-between border-0 bg-transparent px-0 py-2 text-left text-sm font-semibold outline-none shadow-none focus:ring-0; } + + .nav-icon svg { + display: block; + height: 100%; + width: 100%; + } } diff --git a/docs/changelog.md b/docs/changelog.md index dbdda33..46e7f55 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -61,3 +61,5 @@ - 기본 카테고리 초기값을 상위 카테고리 `기본 수입`, `기본 지출` 1개씩으로 단순화했다. - 카테고리/자산 추가 바텀 시트의 닫기 버튼을 제거하고 추가 실패 안내를 표시하도록 했다. - 개발 환경에서만 노출되는 테스트 로그인 버튼과 개발용 로그인 API를 추가했다. +- 카테고리 상세 화면에 삭제 동작을 추가하고, 하위 카테고리나 거래가 연결된 카테고리는 삭제하지 못하도록 안내를 추가했다. +- 하단 네비게이션과 PC 사이드바 SVG 아이콘을 마스크 방식 대신 인라인 SVG 렌더링으로 변경해 네모 박스로 보이는 문제를 수정했다. diff --git a/layouts/default.vue b/layouts/default.vue index bb5f6da..f3fc8be 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -21,7 +21,7 @@ :class="isActive(item.to) ? 'bg-red-100' : ''" aria-hidden="true" > - + {{ item.label }} @@ -50,7 +50,7 @@ :class="isActive(item.to) ? 'bg-red-50' : ''" aria-hidden="true" > - + {{ item.label }} @@ -68,18 +68,22 @@ diff --git a/pages/settings.vue b/pages/settings.vue index 74eecf6..693f9e2 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -263,7 +263,11 @@ - +

{{ categoryMessage }}

+
+ + +
@@ -453,6 +457,7 @@ const categoryEditForm = reactive({ icon: 'circle', color: '#6b7673' }) +const categoryMessage = ref('') const categoryIconOptions = CATEGORY_ICON_OPTIONS const categoryColorOptions = CATEGORY_COLOR_OPTIONS @@ -502,6 +507,7 @@ watch(selectedCategory, (category: any) => { icon: category.icon || 'circle', color: category.color || '#6b7673' }) + categoryMessage.value = '' }) watch(selectedAsset, (asset: any) => { @@ -520,6 +526,7 @@ function open(nextView: SettingsView) { selectedCategoryId.value = '' selectedAssetId.value = '' assetMessage.value = '' + categoryMessage.value = '' if (nextView === 'account') { accountForm.name = session.value?.name || '' accountMessage.value = '' @@ -662,6 +669,7 @@ async function createCategory() { async function saveCategory() { if (!selectedCategory.value) return + categoryMessage.value = '' await $fetch(`/api/categories/${selectedCategory.value.id}`, { method: 'PATCH', body: categoryEditForm @@ -670,6 +678,30 @@ async function saveCategory() { selectedCategoryId.value = '' } +async function deleteCategory() { + if (!selectedCategory.value) return + categoryMessage.value = '' + try { + await $fetch(`/api/categories/${selectedCategory.value.id}`, { method: 'DELETE' }) + const deletedParentId = selectedCategory.value.parentId + await refreshCategories() + selectedCategoryId.value = '' + if (!deletedParentId) selectedParentId.value = '' + } catch (err: any) { + const status = err?.statusCode || err?.response?.status + const statusMessage = err?.statusMessage || err?.response?._data?.statusMessage || '' + if (status === 409 && statusMessage === 'Category has children') { + categoryMessage.value = '하위 카테고리가 있는 상위 카테고리는 삭제할 수 없습니다.' + return + } + if (status === 409) { + categoryMessage.value = '거래가 연결된 카테고리는 삭제할 수 없습니다.' + return + } + categoryMessage.value = '카테고리를 삭제하지 못했습니다.' + } +} + async function createShareLink() { await $fetch('/api/share-links', { method: 'POST', diff --git a/server/api/categories/[id].delete.ts b/server/api/categories/[id].delete.ts new file mode 100644 index 0000000..e095b6a --- /dev/null +++ b/server/api/categories/[id].delete.ts @@ -0,0 +1,26 @@ +import { getRouterParam, createError } from 'h3' +import { prisma } from '../../utils/db' +import { requireUser } from '../../utils/auth' + +export default defineEventHandler(async (event) => { + const user = await requireUser(event) + const id = getRouterParam(event, 'id')! + const category = await prisma.category.findFirst({ where: { id, userId: user.id } }) + if (!category) throw createError({ statusCode: 404, statusMessage: 'Category not found' }) + + const [childCount, transactionCount] = await Promise.all([ + prisma.category.count({ where: { parentId: id, userId: user.id } }), + prisma.transaction.count({ where: { categoryId: id, userId: user.id } }) + ]) + + if (childCount) { + throw createError({ statusCode: 409, statusMessage: 'Category has children' }) + } + + if (transactionCount) { + throw createError({ statusCode: 409, statusMessage: 'Category has transactions' }) + } + + await prisma.category.delete({ where: { id } }) + return { ok: true } +}) diff --git a/todo.md b/todo.md index 5e3a987..cafe604 100644 --- a/todo.md +++ b/todo.md @@ -27,9 +27,7 @@ ## 4. 카테고리 -- 상위 카테고리와 하위 카테고리의 추가, 수정, 삭제 화면을 완성한다. - 카테고리 정렬 순서 변경 기능을 추가한다. -- 사용 중인 카테고리 삭제 시 처리 규칙을 정한다. ## 5. 자산