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 @@