Merge branch 'dev'
This commit is contained in:
@@ -7,6 +7,7 @@ import AppBottomNav from '@/components/layout/AppBottomNav.vue'
|
|||||||
import LoginModal from '@/components/LoginModal.vue'
|
import LoginModal from '@/components/LoginModal.vue'
|
||||||
import SignupModal from '@/components/SignupModal.vue'
|
import SignupModal from '@/components/SignupModal.vue'
|
||||||
import WebOnlyNotice from '@/components/WebOnlyNotice.vue'
|
import WebOnlyNotice from '@/components/WebOnlyNotice.vue'
|
||||||
|
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useUiStore } from '@/stores/ui'
|
import { useUiStore } from '@/stores/ui'
|
||||||
@@ -58,6 +59,9 @@ watch(() => route.fullPath, () => ui.closeSidebar())
|
|||||||
<LoginModal />
|
<LoginModal />
|
||||||
<SignupModal />
|
<SignupModal />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- 공용 인앱 다이얼로그 (네이티브 alert/confirm/prompt 대체) -->
|
||||||
|
<AppDialog />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<script setup>
|
||||||
|
import { nextTick, watch, ref } from 'vue'
|
||||||
|
import { dialogState, dialogConfirm, dialogCancel } from '@/composables/dialog'
|
||||||
|
|
||||||
|
const inputEl = ref(null)
|
||||||
|
// prompt 열릴 때 입력칸 포커스
|
||||||
|
watch(
|
||||||
|
() => dialogState.open,
|
||||||
|
async (open) => {
|
||||||
|
if (open && dialogState.type === 'prompt') {
|
||||||
|
await nextTick()
|
||||||
|
inputEl.value?.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="dlg-fade">
|
||||||
|
<div v-if="dialogState.open" class="dlg-backdrop" @click.self="dialogCancel">
|
||||||
|
<div class="dlg" role="dialog" aria-modal="true">
|
||||||
|
<h2 class="dlg-title">{{ dialogState.title }}</h2>
|
||||||
|
<p v-if="dialogState.message" class="dlg-msg">{{ dialogState.message }}</p>
|
||||||
|
<input
|
||||||
|
v-if="dialogState.type === 'prompt'"
|
||||||
|
ref="inputEl"
|
||||||
|
v-model="dialogState.inputValue"
|
||||||
|
class="dlg-input"
|
||||||
|
:placeholder="dialogState.placeholder"
|
||||||
|
@keyup.enter="dialogConfirm"
|
||||||
|
/>
|
||||||
|
<div class="dlg-buttons">
|
||||||
|
<button v-if="dialogState.type !== 'alert'" type="button" class="dlg-btn" @click="dialogCancel">
|
||||||
|
{{ dialogState.cancelText }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="dlg-btn primary" :class="{ danger: dialogState.danger }" @click="dialogConfirm">
|
||||||
|
{{ dialogState.okText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dlg-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.dlg {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
padding: 1.4rem 1.25rem calc(1.25rem + env(safe-area-inset-bottom));
|
||||||
|
background: var(--color-background);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.dlg-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
}
|
||||||
|
.dlg-msg {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.dlg-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.dlg-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.dlg-btn {
|
||||||
|
padding: 0.45rem 1rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dlg-btn.primary {
|
||||||
|
background: hsla(160, 100%, 37%, 1);
|
||||||
|
border-color: hsla(160, 100%, 37%, 1);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dlg-btn.primary.danger {
|
||||||
|
background: #c0392b;
|
||||||
|
border-color: #c0392b;
|
||||||
|
}
|
||||||
|
.dlg-fade-enter-active,
|
||||||
|
.dlg-fade-leave-active {
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.dlg-fade-enter-from,
|
||||||
|
.dlg-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -7,6 +7,14 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
import { setupCapacitor } from './capacitor'
|
import { setupCapacitor } from './capacitor'
|
||||||
import { useAuthStore } from './stores/auth'
|
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)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { adminApi } from '@/api/adminApi'
|
import { adminApi } from '@/api/adminApi'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const dialog = useDialog()
|
||||||
const myId = computed(() => auth.user?.id)
|
const myId = computed(() => auth.user?.id)
|
||||||
|
|
||||||
const members = ref([])
|
const members = ref([])
|
||||||
@@ -89,7 +91,7 @@ async function changeStatus(m, status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(m) {
|
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 {
|
try {
|
||||||
await adminApi.removeMember(m.id)
|
await adminApi.removeMember(m.id)
|
||||||
members.value = members.value.filter((x) => x.id !== m.id)
|
members.value = members.value.filter((x) => x.id !== m.id)
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const tags = ref([])
|
const tags = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -79,7 +81,7 @@ async function saveTag(tag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeTag(tag) {
|
async function removeTag(tag) {
|
||||||
if (!confirm(`'${tag.name}' 태그를 삭제할까요? (내역에서 연결도 해제됩니다)`)) return
|
if (!(await dialog.confirm(`'${tag.name}' 태그를 삭제할까요?\n내역에서 연결도 해제됩니다.`, { title: '태그 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await accountApi.removeTag(tag.id)
|
await accountApi.removeTag(tag.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
||||||
import { cardNotif } from '@/native/cardNotif'
|
import { cardNotif } from '@/native/cardNotif'
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
// @capacitor/camera 는 네이티브에서만 동적 로드 (웹은 file input 사용 — 정적 import 시 모바일 웹 청크 로드 실패 유발)
|
// @capacitor/camera 는 네이티브에서만 동적 로드 (웹은 file input 사용 — 정적 import 시 모바일 웹 청크 로드 실패 유발)
|
||||||
|
|
||||||
|
const dialog = useDialog()
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = ref(now.getFullYear())
|
const year = ref(now.getFullYear())
|
||||||
const month = ref(now.getMonth() + 1)
|
const month = ref(now.getMonth() + 1)
|
||||||
@@ -570,7 +572,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(e) {
|
async function remove(e) {
|
||||||
if (!confirm('이 내역을 삭제하시겠습니까?')) return
|
if (!(await dialog.confirm('이 내역을 삭제하시겠습니까?', { title: '내역 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await accountApi.remove(e.id)
|
await accountApi.remove(e.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { onBeforeUnmount, onMounted, reactive, ref, watch, nextTick } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: 'BANK', label: '은행계좌' },
|
{ key: 'BANK', label: '은행계좌' },
|
||||||
@@ -285,7 +287,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(w) {
|
async function remove(w) {
|
||||||
if (!confirm(`'${w.name}' 을(를) 삭제할까요?`)) return
|
if (!(await dialog.confirm(`'${w.name}' 을(를) 삭제할까요?`, { title: '삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await accountApi.removeWallet(w.id)
|
await accountApi.removeWallet(w.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = ref(now.getFullYear())
|
const year = ref(now.getFullYear())
|
||||||
@@ -224,7 +226,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(s) {
|
async function remove(s) {
|
||||||
if (!confirm(`'${s.category}' 예산을 삭제할까요?`)) return
|
if (!(await dialog.confirm(`'${s.category}' 예산을 삭제할까요?`, { title: '예산 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await accountApi.removeBudget(s.id)
|
await accountApi.removeBudget(s.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const recurrings = ref([])
|
const recurrings = ref([])
|
||||||
const wallets = ref([])
|
const wallets = ref([])
|
||||||
@@ -224,7 +226,7 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(r) {
|
async function remove(r) {
|
||||||
if (!confirm(`'${r.title}' 고정 지출를 삭제할까요? (이미 생성된 내역은 유지됩니다)`)) return
|
if (!(await dialog.confirm(`'${r.title}' 고정 지출를 삭제할까요?\n이미 생성된 내역은 유지됩니다.`, { title: '고정지출 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await accountApi.removeRecurring(r.id)
|
await accountApi.removeRecurring(r.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { adminApi } from '@/api/adminApi'
|
import { adminApi } from '@/api/adminApi'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
|
|
||||||
|
const dialog = useDialog()
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
@@ -62,7 +64,7 @@ async function saveCategory(cat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeCategory(cat) {
|
async function removeCategory(cat) {
|
||||||
if (!confirm(`'${cat.name}' 카테고리와 소속 태그를 모두 삭제할까요?`)) return
|
if (!(await dialog.confirm(`'${cat.name}' 카테고리와 소속 태그를 모두 삭제할까요?`, { title: '카테고리 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await adminApi.removeCategory(cat.id)
|
await adminApi.removeCategory(cat.id)
|
||||||
await load()
|
await load()
|
||||||
@@ -84,7 +86,7 @@ async function addTag(cat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeTag(tag) {
|
async function removeTag(tag) {
|
||||||
if (!confirm(`'${tag.name}' 태그를 삭제할까요?`)) return
|
if (!(await dialog.confirm(`'${tag.name}' 태그를 삭제할까요?`, { title: '태그 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await adminApi.removeTag(tag.id)
|
await adminApi.removeTag(tag.id)
|
||||||
await load()
|
await load()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { onMounted, ref, computed } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { boardApi } from '@/api/boardApi'
|
import { boardApi } from '@/api/boardApi'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
import { formatRelative } from '@/utils/datetime'
|
import { formatRelative } from '@/utils/datetime'
|
||||||
import MarkdownViewer from '@/components/editor/MarkdownViewer.vue'
|
import MarkdownViewer from '@/components/editor/MarkdownViewer.vue'
|
||||||
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
import MarkdownEditor from '@/components/editor/MarkdownEditor.vue'
|
||||||
@@ -12,6 +13,7 @@ import { DEFAULT_BOARD } from '@/constants/boards'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const postId = route.params.id
|
const postId = route.params.id
|
||||||
// 게시판(카테고리): URL 우선, 없으면 글의 category, 그래도 없으면 기본
|
// 게시판(카테고리): URL 우선, 없으면 글의 category, 그래도 없으면 기본
|
||||||
@@ -49,7 +51,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removePost() {
|
async function removePost() {
|
||||||
if (!confirm('이 게시글을 삭제하시겠습니까?')) return
|
if (!(await dialog.confirm('이 게시글을 삭제하시겠습니까?', { title: '게시글 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await boardApi.remove(postId)
|
await boardApi.remove(postId)
|
||||||
router.replace(`/board/${category.value}`)
|
router.replace(`/board/${category.value}`)
|
||||||
@@ -59,7 +61,7 @@ async function removePost() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function blockPost() {
|
async function blockPost() {
|
||||||
const reason = prompt('열람 제한 사유 (선택):', '')
|
const reason = await dialog.prompt('열람 제한 사유 (선택):', { title: '열람 제한', placeholder: '사유(선택)' })
|
||||||
if (reason === null) return
|
if (reason === null) return
|
||||||
try {
|
try {
|
||||||
await boardApi.block(postId, reason)
|
await boardApi.block(postId, reason)
|
||||||
@@ -94,7 +96,7 @@ async function addComment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeComment(c) {
|
async function removeComment(c) {
|
||||||
if (!confirm('댓글을 삭제하시겠습니까?')) return
|
if (!(await dialog.confirm('댓글을 삭제하시겠습니까?', { title: '댓글 삭제', danger: true }))) return
|
||||||
try {
|
try {
|
||||||
await boardApi.removeComment(c.id)
|
await boardApi.removeComment(c.id)
|
||||||
post.value.comments = post.value.comments.filter((x) => x.id !== c.id)
|
post.value.comments = post.value.comments.filter((x) => x.id !== c.id)
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
|
import { useDialog } from '@/composables/dialog'
|
||||||
|
|
||||||
|
const dialog = useDialog()
|
||||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '-'
|
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '-'
|
||||||
const clearing = ref(false)
|
const clearing = ref(false)
|
||||||
|
|
||||||
async function clearAppData() {
|
async function clearAppData() {
|
||||||
if (clearing.value) return
|
if (clearing.value) return
|
||||||
const ok = window.confirm(
|
const ok = await dialog.confirm(
|
||||||
'앱에 저장된 모든 데이터(로그인 정보·캐시)를 삭제합니다.\n' +
|
'앱에 저장된 모든 데이터(로그인 정보·캐시)를 삭제합니다.\n삭제 후 로그아웃되며 첫 화면으로 이동합니다.\n계속하시겠습니까?',
|
||||||
'삭제 후 로그아웃되며 첫 화면으로 이동합니다.\n계속하시겠습니까?',
|
{ title: 'App Data 삭제', okText: '삭제', danger: true },
|
||||||
)
|
)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
clearing.value = true
|
clearing.value = true
|
||||||
|
|||||||
Reference in New Issue
Block a user