From a92063dd6446fe17eb87f63b4e0739cc1af5933d Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 27 Jun 2026 21:09:34 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20PC=20=EC=84=A4=EC=B9=98=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(UAC=20=ED=94=84=EB=A1=AC=ED=94=84?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - installer.nsh: 기본 설치경로를 %LOCALAPPDATA%\Programs\DonDwaeji 로 (perMachine=false 와 함께 관리자 권한 불필요 → '디바이스 변경 허용' UAC 안 뜸) Co-Authored-By: Claude Opus 4.8 --- build/installer.nsh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/build/installer.nsh b/build/installer.nsh index 5e6e6bd..2299370 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -1,11 +1,9 @@ -; 기본 설치 경로 지정. -; productName 이 한글이라 electron-builder 가 기본 경로를 package.json name(sb_pt)으로 폴백하는 문제 회피. -; → $PROGRAMFILES\DonDwaeji 로 고정 (사용자는 설치 시 변경 가능). +; 기본 설치 경로 지정 (사용자 폴더 = 관리자 권한 불필요 → UAC 프롬프트 없음). +; productName 이 한글이라 electron-builder 가 기본 경로를 package.json name(sb_pt)으로 폴백하는 문제도 함께 회피. +; → %LOCALAPPDATA%\Programs\DonDwaeji 로 고정 (사용자는 설치 시 변경 가능). !macro preInit SetRegView 64 - WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES64\DonDwaeji" - WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES64\DonDwaeji" + WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LOCALAPPDATA\Programs\DonDwaeji" SetRegView 32 - WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES\DonDwaeji" - WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$PROGRAMFILES\DonDwaeji" + WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LOCALAPPDATA\Programs\DonDwaeji" !macroend From ef1d718fb850dc4122926f0502c64f15e02c93fa Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 27 Jun 2026 21:38:07 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=ED=86=B5=EA=B3=84=20=EB=B6=84?= =?UTF-8?q?=EB=A5=98=EB=B3=84=20=EC=A7=91=EA=B3=84=EB=A5=BC=20=EB=8C=80?= =?UTF-8?q?=EB=B6=84=EB=A5=98=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20(?= =?UTF-8?q?=EC=86=8C=EB=B6=84=EB=A5=98=20=ED=8E=BC=EC=B9=A8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EB=A1=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - catByMajor: 소분류를 부모 대분류로 합산해 파이/범례 표기 - 중복되던 '대분류 합계' 섹션 제거 Co-Authored-By: Claude Opus 4.8 --- src/views/account/AccountDashboardView.vue | 55 +++++++++------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/src/views/account/AccountDashboardView.vue b/src/views/account/AccountDashboardView.vue index eb5057e..760cde1 100644 --- a/src/views/account/AccountDashboardView.vue +++ b/src/views/account/AccountDashboardView.vue @@ -114,12 +114,32 @@ function hideTip() { const PIE_R = 46 // viewBox 120 기준, 선두께 20(hover 24) 포함해도 잘리지 않도록 (46+12=58 ≤ 60) const PIE_C = 2 * Math.PI * PIE_R const PIE_COLORS = ['#3b82a6', '#e67e22', '#2e7d32', '#9b59b6', '#c0392b', '#16a085', '#f39c12', '#8e44ad', '#27ae60', '#d35400', '#2980b9', '#7f8c8d'] -const catTotal = computed(() => catData.value.reduce((s, d) => s + d.total, 0)) +// 분류별 집계를 대분류 기준으로 묶는다 (소분류까지 펼치면 항목이 너무 많아 스크롤이 길어짐). +// 소분류는 부모(대분류)명으로 합산, 매핑이 없으면 자기 자신을 대분류로 본다. +const catByMajor = computed(() => { + const list = categoryList.value.filter((c) => c.type === catType.value) + const byId = {} + list.forEach((c) => (byId[c.id] = c)) + const toMajor = (name) => { + const c = list.find((x) => x.name === name) + if (c && c.parentId != null && byId[c.parentId]) return byId[c.parentId].name + return name + } + const map = {} + for (const d of catData.value) { + const major = toMajor(d.category) + map[major] = (map[major] || 0) + d.total + } + return Object.entries(map) + .map(([category, total]) => ({ category, total })) + .sort((a, b) => b.total - a.total) +}) +const catTotal = computed(() => catByMajor.value.reduce((s, d) => s + d.total, 0)) const pieSegments = computed(() => { const total = catTotal.value if (total <= 0) return [] let acc = 0 - return catData.value.map((d, i) => { + return catByMajor.value.map((d, i) => { const len = (d.total / total) * PIE_C const seg = { category: d.category, @@ -196,26 +216,6 @@ async function loadCategoryList() { categoryList.value = [] } } -// 소분류명 → 대분류명 매핑(현재 구분). 소분류가 아니면 자기 자신. -const majorRollup = computed(() => { - const list = categoryList.value.filter((c) => c.type === catType.value) - const byId = {} - list.forEach((c) => (byId[c.id] = c)) - const toMajor = (name) => { - const c = list.find((x) => x.name === name) - if (c && c.parentId != null && byId[c.parentId]) return byId[c.parentId].name - return name - } - const map = {} - for (const d of catData.value) { - const major = toMajor(d.category) - map[major] = (map[major] || 0) + d.total - } - const rows = Object.entries(map).map(([category, total]) => ({ category, total })).sort((a, b) => b.total - a.total) - // 실제로 묶인 게 있을 때만(소분류 존재) 노출 - const hasSub = list.some((c) => c.parentId != null) && rows.length < catData.value.length - return hasSub ? rows : [] -}) function setCatType(t) { catType.value = t loadCat() @@ -422,16 +422,7 @@ onMounted(async () => { -
-
대분류 합계
-
    -
  • - {{ m.category }} - {{ won(m.total) }} -
  • -
-
-

{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.

+

{{ catType === 'EXPENSE' ? '지출' : '수입' }} 내역이 없습니다.

From f07cb349e22a8b12b6b63eb2628110bcc5a7b430 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 27 Jun 2026 22:07:03 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=ED=8C=90=20?= =?UTF-8?q?=EA=B3=B5=EC=A7=80=20UI=20(=EA=B4=80=EB=A6=AC=EC=9E=90)=20?= =?UTF-8?q?=E2=80=94=20=EC=9E=91=EC=84=B1=20=EC=B2=B4=ED=81=AC=EB=B0=95?= =?UTF-8?q?=EC=8A=A4/=EC=83=81=EC=84=B8=20=ED=86=A0=EA=B8=80/=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=B0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BoardWriteView: 관리자가 커뮤니티 새 글 작성 시 '공지로 등록' 체크박스 - BoardDetailView: 관리자 공지 등록/해제 버튼 + 제목 공지 배지 - BoardListView: 공지 배지 + 행 강조(최상단은 백엔드 정렬) - boardApi: setNotice/unsetNotice Co-Authored-By: Claude Opus 4.8 --- src/api/boardApi.js | 7 +++++++ src/views/board/BoardDetailView.vue | 28 +++++++++++++++++++++++++++- src/views/board/BoardListView.vue | 19 ++++++++++++++++++- src/views/board/BoardWriteView.vue | 24 ++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/api/boardApi.js b/src/api/boardApi.js index bc68a89..0b97b62 100644 --- a/src/api/boardApi.js +++ b/src/api/boardApi.js @@ -53,4 +53,11 @@ export const boardApi = { unblock(id) { return http.post(`/board/posts/${id}/unblock`) }, + // 공지 설정/해제 (관리자) — 목록 최상단 고정 + setNotice(id) { + return http.post(`/board/posts/${id}/notice`) + }, + unsetNotice(id) { + return http.post(`/board/posts/${id}/unnotice`) + }, } diff --git a/src/views/board/BoardDetailView.vue b/src/views/board/BoardDetailView.vue index 34b3dcf..e3cb173 100644 --- a/src/views/board/BoardDetailView.vue +++ b/src/views/board/BoardDetailView.vue @@ -80,6 +80,15 @@ async function unblockPost() { } } +async function setNoticePost(on) { + try { + await (on ? boardApi.setNotice(postId) : boardApi.unsetNotice(postId)) + await load() + } catch (e) { + alert(e.response?.data?.message || '처리에 실패했습니다.') + } +} + async function addComment() { const content = commentText.value.trim() if (!content) return @@ -115,7 +124,7 @@ onMounted(load)
-

{{ post.title }}

+

공지{{ post.title }}

{{ post.authorName }} · {{ formatRelative(post.createdAt) }} @@ -144,6 +153,9 @@ onMounted(load)
+ + + @@ -268,6 +280,20 @@ button.warn { border-color: #e67e22; color: #e67e22; } +button.notice-btn { + border-color: hsla(160, 100%, 37%, 1); + color: hsla(160, 100%, 37%, 1); +} +.notice-badge { + margin-right: 0.4rem; + padding: 0.1rem 0.45rem; + border-radius: 4px; + background: hsla(160, 100%, 37%, 1); + color: #fff; + font-size: 0.8rem; + font-weight: 700; + vertical-align: middle; +} .actions { display: flex; justify-content: space-between; diff --git a/src/views/board/BoardListView.vue b/src/views/board/BoardListView.vue index d09a562..3078767 100644 --- a/src/views/board/BoardListView.vue +++ b/src/views/board/BoardListView.vue @@ -163,8 +163,9 @@ onMounted(loadTags) - + + 공지 @@ -318,6 +319,22 @@ button:disabled { color: #c0392b; font-size: 0.72rem; } +.notice-badge { + margin-right: 0.4rem; + padding: 0.05rem 0.4rem; + border-radius: 3px; + background: hsla(160, 100%, 37%, 1); + color: #fff; + font-size: 0.72rem; + font-weight: 700; + vertical-align: middle; +} +.notice-row { + background: hsla(160, 100%, 37%, 0.06); +} +.notice-row .title { + font-weight: 600; +} /* 하단 영역: 페이징(가운데) + 돋보기(오른쪽) 같은 행 */ .list-footer { diff --git a/src/views/board/BoardWriteView.vue b/src/views/board/BoardWriteView.vue index 277a6d6..9c3e058 100644 --- a/src/views/board/BoardWriteView.vue +++ b/src/views/board/BoardWriteView.vue @@ -2,19 +2,25 @@ import { onMounted, ref, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { boardApi } from '@/api/boardApi' +import { useAuthStore } from '@/stores/auth' import MarkdownEditor from '@/components/editor/MarkdownEditor.vue' import IconBtn from '@/components/ui/IconBtn.vue' import { DEFAULT_BOARD } from '@/constants/boards' const route = useRoute() const router = useRouter() +const auth = useAuthStore() const category = route.params.category || DEFAULT_BOARD const editId = route.params.id || null const isEdit = computed(() => !!editId) +// 공지: 관리자가 커뮤니티 게시판에 새 글 작성할 때만 설정 가능 +const isAdmin = computed(() => auth.user?.role === 'ADMIN') +const canNotice = computed(() => isAdmin.value && category === 'community' && !isEdit.value) const title = ref('') const content = ref('') +const notice = ref(false) const tagGroups = ref([]) const selectedTagIds = ref([]) const loading = ref(false) @@ -65,6 +71,7 @@ async function submit() { title: title.value.trim(), content: content.value, category, + notice: canNotice.value ? notice.value : false, tagIds: selectedTagIds.value, } try { @@ -97,6 +104,12 @@ onMounted(async () => {
+ + +
@@ -178,6 +191,17 @@ h1 { font-size: 0.85rem; opacity: 0.65; } +.notice-check { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.9rem; + cursor: pointer; +} +.notice-check input { + width: auto; + cursor: pointer; +} .buttons { display: flex; justify-content: flex-end; From 63b4792e5859bd578c27ae090a43d7e050660519 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 27 Jun 2026 22:08:51 +0900 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20=EC=9B=B9=20=EC=95=88=EB=82=B4?= =?UTF-8?q?=EB=AC=B8=20=EC=A4=84=EB=B0=94=EA=BF=88=EC=9D=84=20=EC=A0=88=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=EB=A1=9C=20(inline-block=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'PC는 설치형 앱으로,' / '모바일은 앱으로 이용해요.' 등 절 경계에서만 줄바꿈 Co-Authored-By: Claude Opus 4.8 --- src/components/WebOnlyNotice.vue | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/WebOnlyNotice.vue b/src/components/WebOnlyNotice.vue index c45606f..a6ce3a0 100644 --- a/src/components/WebOnlyNotice.vue +++ b/src/components/WebOnlyNotice.vue @@ -13,8 +13,14 @@ const mobileReady = false

슬림하게 관리하는 가계부 · 자산 · 예산

-

🖥️ PC는 설치형 앱으로, 📱 모바일은 으로 이용해요.

-

웹 브라우저에서는 안내만 제공됩니다. 설치한 뒤 로그인해 주세요.

+

+ 🖥️ PC는 설치형 앱으로, + 📱 모바일은 으로 이용해요. +

+

+ 웹 브라우저에서는 안내만 제공됩니다. + 설치한 뒤 로그인해 주세요. +

@@ -78,6 +84,11 @@ const mobileReady = false font-size: 0.86rem; opacity: 0.7; } +/* 각 구절을 한 덩어리로 — 좁으면 절(span) 경계에서만 줄바꿈, 넓으면 한 줄 */ +.lead .ln, +.sub .ln { + display: inline-block; +} .dl-group { display: flex; flex-direction: column; From 75228cbb641d24b760b28f0b8430b822cd381106 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sat, 27 Jun 2026 22:15:27 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=B8=8C=EB=9E=9C=EB=93=9C/=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=ED=8B=80=20+=20=EB=9D=BC=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=C2=B7=EB=8B=A4=ED=81=AC=20=ED=85=8C=EB=A7=88=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 사이드바 상단에 돼지 로고 + '돈돼지 가계부'('메뉴' 텍스트 대체, 데스크톱/모바일 공통) 2) 헤더에서 돼지 로고 제거 → 현재 화면 타이틀 표시(라우트별) 3) 설정에 화면 테마(시스템/라이트/다크) 선택 추가 - base.css: data-theme 기반 수동 다크 + 시스템 설정 폴백 - theme.js 유틸 + ui 스토어 theme 상태, main.js 시작 시 적용(깜빡임 방지) Co-Authored-By: Claude Opus 4.8 --- src/assets/base.css | 18 +++++++- src/components/layout/AppHeader.vue | 53 ++++++++++++++---------- src/components/layout/AppSidebar.vue | 61 +++++++++++++++++++--------- src/main.js | 4 ++ src/stores/ui.js | 9 ++++ src/theme.js | 34 ++++++++++++++++ src/views/settings/SettingsView.vue | 50 +++++++++++++++++++++++ 7 files changed, 187 insertions(+), 42 deletions(-) create mode 100644 src/theme.js diff --git a/src/assets/base.css b/src/assets/base.css index 8816868..b09de53 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -36,8 +36,22 @@ --section-gap: 160px; } +/* 다크 변수 묶음 — 명시적 다크(data-theme=dark)와 시스템 다크에서 공통 사용 */ +:root[data-theme='dark'] { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-1); +} + +/* 시스템 설정 따름(테마 미지정 = 시스템). 명시적 라이트/다크가 있으면 그게 우선 */ @media (prefers-color-scheme: dark) { - :root { + :root:not([data-theme]) { --color-background: var(--vt-c-black); --color-background-soft: var(--vt-c-black-soft); --color-background-mute: var(--vt-c-black-mute); @@ -46,7 +60,7 @@ --color-border-hover: var(--vt-c-divider-dark-1); --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); + --color-text: var(--vt-c-text-dark-1); } } diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index 844eecd..2451b2a 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -1,12 +1,39 @@