feat: alert/confirm을 공통 모달 다이얼로그로 통합

모든 window.alert/confirm 을 업무 삭제 모달과 동일한 구조/디자인의
공통 모달로 대체하고, 한 곳에서 관리하도록 다이얼로그 시스템을 도입한다.

- dialog.store: 알림/확인 큐 + Promise 기반 상태 관리
- useDialog(): alert/confirm 컴포저블 API
- AppDialog: App 루트 1회 마운트, 전역 .modal-*/.mbtn 클래스로 렌더
  (variant: info/success/warning/danger → 아이콘·버튼 색)
- relay.css: modal-ico.info/.warning, mbtn.primary 변형 추가
- 교체: API 에러 알림(useApi), 검증/성공 알림(Signup/ProjectCreate/
  ProjectSettings/TaskCreate), 삭제·제거 확인(ProjectSettings/
  ProjectMembers/AgentChatPanel)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 22:46:28 +09:00
parent 118d12f829
commit b1afe5480c
12 changed files with 284 additions and 16 deletions
@@ -4,9 +4,11 @@ import { ref } from 'vue'
import { useRouter, RouterLink } from 'vue-router'
import AppShell from '@/layouts/AppShell.vue'
import { useProjectStore } from '@/stores/project.store'
import { useDialog } from '@/composables/useDialog'
const router = useRouter()
const projectStore = useProjectStore()
const dialog = useDialog()
const name = ref('')
const description = ref('')
@@ -16,7 +18,7 @@ const submitting = ref(false)
async function createProject() {
if (submitting.value) return
if (!name.value.trim()) {
window.alert('프로젝트 이름을 입력해 주세요.')
void dialog.alert('프로젝트 이름을 입력해 주세요.', { variant: 'warning' })
return
}
submitting.value = true
@@ -9,6 +9,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
import { useProjectStore } from '@/stores/project.store'
import { useMemberStore } from '@/stores/member.store'
import { useUser } from '@/composables/useUser'
import { useDialog } from '@/composables/useDialog'
import type { Project } from '@/mock/relay.mock'
import type { ApiMember } from '@/types/repo'
import type { MemberRoleType } from '@/types/member'
@@ -19,6 +20,7 @@ const route = useRoute()
const projectId = computed(() => String(route.params.projectId))
const projectStore = useProjectStore()
const memberStore = useMemberStore()
const dialog = useDialog()
// 프로젝트 헤더 — API 연동
const repo = ref<Project | null>(null)
@@ -96,7 +98,11 @@ async function changeRole(userId: string, roleType: MemberRoleType) {
// --- 멤버 제거 ---
async function removeMember(userId: string, name: string) {
if (!window.confirm(`${name} 님을 프로젝트에서 제거하시겠습니까?`)) return
const ok = await dialog.confirm(`${name} 님을 프로젝트에서 제거할까요?`, {
variant: 'danger',
confirmText: '제거',
})
if (!ok) return
try {
await memberStore.remove(projectId.value, userId)
} catch {
@@ -6,12 +6,14 @@ import AppShell from '@/layouts/AppShell.vue'
import ProjectHeader from '@/components/ProjectHeader.vue'
import ProjectTabs from '@/components/ProjectTabs.vue'
import { useProjectStore } from '@/stores/project.store'
import { useDialog } from '@/composables/useDialog'
import { type Project } from '@/mock/relay.mock'
const route = useRoute()
const router = useRouter()
const projectId = computed(() => String(route.params.projectId))
const projectStore = useProjectStore()
const dialog = useDialog()
const repo = ref<Project | null>(null)
@@ -56,7 +58,7 @@ async function save() {
name: displayTitle.value.trim(),
description: description.value.trim(),
})
window.alert('변경 사항을 저장했습니다.')
void dialog.alert('변경 사항을 저장했습니다.', { variant: 'success' })
} catch {
// 인터셉터 전역 처리
} finally {
@@ -67,7 +69,15 @@ async function save() {
// 프로젝트 삭제 (소유자 전용)
async function removeProject() {
if (!repo.value || !isOwner.value) return
if (!window.confirm('프로젝트와 그 안의 모든 업무·활동 기록이 영구 삭제됩니다. 계속할까요?')) return
const ok = await dialog.confirm(
'프로젝트와 그 안의 모든 업무·활동 기록이 영구 삭제됩니다. 이 작업은 되돌릴 수 없습니다.',
{
title: '프로젝트를 삭제할까요?',
variant: 'danger',
confirmText: '삭제',
},
)
if (!ok) return
try {
await projectStore.remove(projectId.value)
await router.push('/projects')
+6 -4
View File
@@ -3,8 +3,10 @@
import { computed, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth.store'
import { useDialog } from '@/composables/useDialog'
const authStore = useAuthStore()
const dialog = useDialog()
// 폼 상태
const name = ref('')
@@ -30,19 +32,19 @@ async function submit() {
// 클라이언트 1차 검증 (서버 DTO 가 최종 검증)
if (!name.value.trim() || !email.value.trim() || !password.value) {
window.alert('이름, 이메일, 비밀번호를 모두 입력해 주세요.')
void dialog.alert('이름, 이메일, 비밀번호를 모두 입력해 주세요.', { variant: 'warning' })
return
}
if (password.value.length < 8) {
window.alert('비밀번호는 최소 8자 이상이어야 합니다.')
void dialog.alert('비밀번호는 최소 8자 이상이어야 합니다.', { variant: 'warning' })
return
}
if (password.value !== passwordConfirm.value) {
window.alert('비밀번호가 일치하지 않습니다.')
void dialog.alert('비밀번호가 일치하지 않습니다.', { variant: 'warning' })
return
}
if (!agreeTerms.value) {
window.alert('이용약관 및 개인정보 처리방침에 동의해 주세요.')
void dialog.alert('이용약관 및 개인정보 처리방침에 동의해 주세요.', { variant: 'warning' })
return
}
+7 -2
View File
@@ -11,6 +11,7 @@ import { useTaskStore } from '@/stores/task.store'
import { useAuthStore } from '@/stores/auth.store'
import { useTask } from '@/composables/useTask'
import { useAi } from '@/composables/useAi'
import { useDialog } from '@/composables/useDialog'
import { attachmentKindOf, fileBadge, formatBytes } from '@/shared/utils/format'
import { CHECKLIST_CATEGORIES } from '@/mock/relay.mock'
import type {
@@ -36,6 +37,7 @@ const taskStore = useTaskStore()
const authStore = useAuthStore()
const taskApi = useTask()
const ai = useAi()
const dialog = useDialog()
// API 멤버 → 화면용 User(이니셜/색상 파생)
function toUserView(m: ApiMember): User {
@@ -337,7 +339,9 @@ function onPickFiles(e: Event) {
const ok = picked.filter((f) => f.size <= MAX_FILE_SIZE)
pendingFiles.value.push(...ok)
if (ok.length < picked.length) {
window.alert(`25MB를 초과하는 파일 ${picked.length - ok.length}개는 제외했습니다.`)
void dialog.alert(`25MB를 초과하는 파일 ${picked.length - ok.length}개는 제외했습니다.`, {
variant: 'warning',
})
}
input.value = '' // 같은 파일을 다시 선택할 수 있도록 초기화
}
@@ -436,8 +440,9 @@ async function submitTask() {
// 선택한 파일 업로드(업무가 생성/확정된 뒤에야 가능) — 베스트에포트
const failed = await uploadPendingFiles(tid)
if (failed > 0) {
window.alert(
void dialog.alert(
`파일 ${failed}개 업로드에 실패했습니다. 업무 상세 화면에서 다시 첨부해 주세요.`,
{ variant: 'warning' },
)
}
router.push(`/projects/${projectId.value}/tasks/${tid}`)