From f533df0024a34f93002cd9a4e9f154f6f3e0757f Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Wed, 24 Jun 2026 21:57:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20?= =?UTF-8?q?alert/confirm/prompt=20=E2=86=92=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=9D=B8=EC=95=B1=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20(PC=20'sb=5Fpt'=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - composables/dialog + components/ui/AppDialog(App 루트 마운트) - window.alert 전역 오버라이드로 모든 alert 인앱화 - confirm/prompt(동기)는 각 호출부를 await dialog.confirm/prompt 로 교체 (가계부·예산·고정지출·계좌·태그·관리자·게시판·설정 — 삭제/열람제한 등) - 웹/PC/APK 일관 UI, 의미있는 제목 부여 Co-Authored-By: Claude Opus 4.8 --- src/App.vue | 4 + src/components/ui/AppDialog.vue | 121 ++++++++++++++++++++++++ src/composables/dialog.js | 57 +++++++++++ src/main.js | 8 ++ src/views/UsersView.vue | 4 +- src/views/account/AccountTagView.vue | 4 +- src/views/account/AccountView.vue | 4 +- src/views/account/AccountWalletView.vue | 4 +- src/views/account/BudgetView.vue | 4 +- src/views/account/RecurringView.vue | 4 +- src/views/admin/TagAdminView.vue | 6 +- src/views/board/BoardDetailView.vue | 8 +- src/views/settings/SettingsView.vue | 8 +- 13 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 src/components/ui/AppDialog.vue create mode 100644 src/composables/dialog.js diff --git a/src/App.vue b/src/App.vue index 7e0b6e5..bfcdedd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,6 +7,7 @@ import AppBottomNav from '@/components/layout/AppBottomNav.vue' import LoginModal from '@/components/LoginModal.vue' import SignupModal from '@/components/SignupModal.vue' import WebOnlyNotice from '@/components/WebOnlyNotice.vue' +import AppDialog from '@/components/ui/AppDialog.vue' import { Capacitor } from '@capacitor/core' import { useAuthStore } from '@/stores/auth' import { useUiStore } from '@/stores/ui' @@ -58,6 +59,9 @@ watch(() => route.fullPath, () => ui.closeSidebar()) + + + diff --git a/src/composables/dialog.js b/src/composables/dialog.js new file mode 100644 index 0000000..0f9e4d5 --- /dev/null +++ b/src/composables/dialog.js @@ -0,0 +1,57 @@ +import { reactive } from 'vue' + +// 앱 공용 인앱 다이얼로그 상태(싱글턴). 네이티브 alert/confirm/prompt 대체. +// PC(Electron)에서 제목이 'sb_pt'로 뜨던 문제 + 웹/APK 일관 UI 제공. +const state = reactive({ + open: false, + type: 'alert', // 'alert' | 'confirm' | 'prompt' + title: '', + message: '', + okText: '확인', + cancelText: '취소', + danger: false, + inputValue: '', + placeholder: '', + _resolve: null, +}) + +function settle(result) { + const r = state._resolve + state._resolve = null + state.open = false + if (r) r(result) +} + +function alert(message, { title = '알림', okText = '확인' } = {}) { + return new Promise((resolve) => { + Object.assign(state, { type: 'alert', title, message, okText, danger: false, _resolve: resolve, open: true }) + }) +} + +function confirm(message, { title = '확인', okText = '확인', cancelText = '취소', danger = false } = {}) { + return new Promise((resolve) => { + Object.assign(state, { type: 'confirm', title, message, okText, cancelText, danger, _resolve: resolve, open: true }) + }) +} + +function prompt(message, { title = '입력', okText = '확인', cancelText = '취소', defaultValue = '', placeholder = '' } = {}) { + return new Promise((resolve) => { + Object.assign(state, { + type: 'prompt', title, message, okText, cancelText, + inputValue: defaultValue, placeholder, danger: false, _resolve: resolve, open: true, + }) + }) +} + +// AppDialog 컴포넌트에서 사용 +export const dialogState = state +export function dialogConfirm() { + settle(state.type === 'prompt' ? state.inputValue : true) +} +export function dialogCancel() { + settle(state.type === 'prompt' ? null : false) +} + +export function useDialog() { + return { alert, confirm, prompt } +} diff --git a/src/main.js b/src/main.js index 18705f9..7c52f28 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,14 @@ import App from './App.vue' import router from './router' import { setupCapacitor } from './capacitor' import { useAuthStore } from './stores/auth' +import { useDialog } from './composables/dialog' + +// 네이티브 alert 를 인앱 다이얼로그로 대체 (PC 'sb_pt' 제목 제거 + 웹/APK 일관). +// confirm/prompt 는 동기 반환이라 오버라이드 불가 → 각 호출부에서 await dialog.confirm/prompt 사용. +const dialog = useDialog() +window.alert = (message) => { + dialog.alert(message == null ? '' : String(message)) +} const app = createApp(App) diff --git a/src/views/UsersView.vue b/src/views/UsersView.vue index e64b92f..166301e 100644 --- a/src/views/UsersView.vue +++ b/src/views/UsersView.vue @@ -2,9 +2,11 @@ import { computed, onMounted, ref } from 'vue' import { adminApi } from '@/api/adminApi' import { useAuthStore } from '@/stores/auth' +import { useDialog } from '@/composables/dialog' import IconBtn from '@/components/ui/IconBtn.vue' const auth = useAuthStore() +const dialog = useDialog() const myId = computed(() => auth.user?.id) const members = ref([]) @@ -89,7 +91,7 @@ async function changeStatus(m, status) { } async function remove(m) { - if (!confirm(`'${m.name}(${m.loginId})' 회원을 삭제할까요? 되돌릴 수 없습니다.`)) return + if (!(await dialog.confirm(`'${m.name}(${m.loginId})' 회원을 삭제할까요?\n되돌릴 수 없습니다.`, { title: '회원 삭제', danger: true }))) return try { await adminApi.removeMember(m.id) members.value = members.value.filter((x) => x.id !== m.id) diff --git a/src/views/account/AccountTagView.vue b/src/views/account/AccountTagView.vue index b90f000..12e3b7e 100644 --- a/src/views/account/AccountTagView.vue +++ b/src/views/account/AccountTagView.vue @@ -3,9 +3,11 @@ import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue' import { useRouter } from 'vue-router' import Sortable from 'sortablejs' import { accountApi } from '@/api/accountApi' +import { useDialog } from '@/composables/dialog' import IconBtn from '@/components/ui/IconBtn.vue' const router = useRouter() +const dialog = useDialog() const tags = ref([]) const loading = ref(false) @@ -79,7 +81,7 @@ async function saveTag(tag) { } async function removeTag(tag) { - if (!confirm(`'${tag.name}' 태그를 삭제할까요? (내역에서 연결도 해제됩니다)`)) return + if (!(await dialog.confirm(`'${tag.name}' 태그를 삭제할까요?\n내역에서 연결도 해제됩니다.`, { title: '태그 삭제', danger: true }))) return try { await accountApi.removeTag(tag.id) await load() diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index fcf2acd..054f3f3 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -1,12 +1,14 @@