604 lines
23 KiB
Vue
604 lines
23 KiB
Vue
<template>
|
|
<div>
|
|
<AppHeader />
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h4>
|
|
<i class="bi bi-journal-text me-2"></i>
|
|
{{ isEditing ? '회의록 수정' : '회의록 상세' }}
|
|
</h4>
|
|
<p class="text-muted mb-0">
|
|
{{ meeting?.meetingTitle }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<NuxtLink to="/meeting" class="btn btn-outline-secondary me-2">
|
|
<i class="bi bi-arrow-left me-1"></i> 목록
|
|
</NuxtLink>
|
|
<button v-if="!isEditing" class="btn btn-primary" @click="isEditing = true">
|
|
<i class="bi bi-pencil me-1"></i> 수정
|
|
</button>
|
|
<button v-if="!isEditing" class="btn btn-outline-danger ms-2" @click="deleteMeeting">
|
|
<i class="bi bi-trash me-1"></i> 삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="text-center py-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
<p class="mt-2 text-muted">로딩 중...</p>
|
|
</div>
|
|
|
|
<div v-else-if="meeting" class="row">
|
|
<!-- 왼쪽: 기본 정보 -->
|
|
<div class="col-md-4">
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<strong>기본 정보</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">회의 제목</label>
|
|
<input
|
|
v-if="isEditing"
|
|
type="text"
|
|
class="form-control"
|
|
v-model="form.meetingTitle"
|
|
/>
|
|
<p v-else class="form-control-plaintext">{{ meeting.meetingTitle }}</p>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">회의 유형</label>
|
|
<div v-if="isEditing" class="btn-group w-100" role="group">
|
|
<input type="radio" class="btn-check" id="type-project" value="PROJECT" v-model="form.meetingType" />
|
|
<label class="btn btn-outline-primary" for="type-project">프로젝트</label>
|
|
<input type="radio" class="btn-check" id="type-internal" value="INTERNAL" v-model="form.meetingType" />
|
|
<label class="btn btn-outline-info" for="type-internal">내부업무</label>
|
|
</div>
|
|
<p v-else class="form-control-plaintext">
|
|
<span :class="meeting.meetingType === 'PROJECT' ? 'badge bg-primary' : 'badge bg-info'">
|
|
{{ meeting.meetingType === 'PROJECT' ? '프로젝트 회의' : '내부업무 회의' }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mb-3" v-if="form.meetingType === 'PROJECT' || meeting.projectName">
|
|
<label class="form-label">프로젝트</label>
|
|
<select v-if="isEditing && form.meetingType === 'PROJECT'" class="form-select" v-model="form.projectId">
|
|
<option value="">선택</option>
|
|
<option v-for="p in projects" :key="p.projectId" :value="p.projectId">{{ p.projectName }}</option>
|
|
</select>
|
|
<p v-else class="form-control-plaintext">{{ meeting.projectName || '-' }}</p>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">회의 일자</label>
|
|
<input v-if="isEditing" type="date" class="form-control" v-model="form.meetingDate" />
|
|
<p v-else class="form-control-plaintext">{{ formatDate(meeting.meetingDate) }}</p>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<label class="form-label">시작</label>
|
|
<input v-if="isEditing" type="time" class="form-control" v-model="form.startTime" />
|
|
<p v-else class="form-control-plaintext">{{ meeting.startTime?.slice(0,5) || '-' }}</p>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label">종료</label>
|
|
<input v-if="isEditing" type="time" class="form-control" v-model="form.endTime" />
|
|
<p v-else class="form-control-plaintext">{{ meeting.endTime?.slice(0,5) || '-' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">장소</label>
|
|
<input v-if="isEditing" type="text" class="form-control" v-model="form.location" />
|
|
<p v-else class="form-control-plaintext">{{ meeting.location || '-' }}</p>
|
|
</div>
|
|
|
|
<div class="mb-0">
|
|
<label class="form-label">작성자</label>
|
|
<p class="form-control-plaintext">{{ meeting.authorName }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 참석자 -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong>참석자 ({{ attendees.length }}명)</strong>
|
|
<div v-if="isEditing">
|
|
<button class="btn btn-sm btn-outline-primary me-1" @click="showEmployeeModal = true">
|
|
<i class="bi bi-person-plus"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" @click="addExternalAttendee">
|
|
<i class="bi bi-person-plus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<ul class="list-group list-group-flush">
|
|
<li v-for="(att, idx) in displayAttendees" :key="idx" class="list-group-item d-flex justify-content-between">
|
|
<div>
|
|
<i :class="att.isExternal ? 'bi bi-person text-secondary' : 'bi bi-person-fill text-primary'" class="me-1"></i>
|
|
{{ att.isExternal ? att.externalName : att.employeeName }}
|
|
<small v-if="att.isExternal && att.externalCompany" class="text-muted">({{ att.externalCompany }})</small>
|
|
<small v-if="!att.isExternal && att.company" class="text-muted">({{ att.company }})</small>
|
|
</div>
|
|
<button v-if="isEditing" class="btn btn-sm btn-link text-danger" @click="removeAttendee(idx)">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</li>
|
|
<li v-if="displayAttendees.length === 0" class="list-group-item text-center text-muted">
|
|
참석자 없음
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 회의 내용 + AI 분석 -->
|
|
<div class="col-md-8">
|
|
<!-- 회의 내용 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<strong>회의 내용</strong>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<textarea
|
|
v-if="isEditing"
|
|
class="form-control border-0 h-100"
|
|
v-model="form.rawContent"
|
|
style="min-height: 300px; resize: none;"
|
|
></textarea>
|
|
<div v-else class="p-3" style="min-height: 200px; white-space: pre-wrap;">{{ meeting.rawContent || '(내용 없음)' }}</div>
|
|
</div>
|
|
<div v-if="isEditing" class="card-footer d-flex justify-content-end">
|
|
<button class="btn btn-secondary me-2" @click="cancelEdit">취소</button>
|
|
<button class="btn btn-primary" @click="updateMeeting" :disabled="isSaving">
|
|
<span v-if="isSaving">
|
|
<span class="spinner-border spinner-border-sm me-1"></span>저장 중...
|
|
</span>
|
|
<span v-else><i class="bi bi-check-lg me-1"></i>저장</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI 분석 결과 -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<i class="bi bi-robot me-2"></i><strong>AI 분석 결과</strong>
|
|
<span v-if="meeting.aiStatus === 'CONFIRMED'" class="badge bg-success ms-2">확정됨</span>
|
|
<span v-else-if="meeting.aiStatus === 'PENDING'" class="badge bg-warning ms-2">미확정</span>
|
|
<span v-else class="badge bg-secondary ms-2">미분석</span>
|
|
</div>
|
|
<div>
|
|
<button
|
|
v-if="meeting.aiStatus !== 'CONFIRMED'"
|
|
class="btn btn-sm btn-outline-primary me-2"
|
|
@click="analyzeContent"
|
|
:disabled="isAnalyzing"
|
|
>
|
|
<span v-if="isAnalyzing"><span class="spinner-border spinner-border-sm me-1"></span></span>
|
|
<i v-else class="bi bi-magic me-1"></i>
|
|
{{ meeting.aiStatus === 'PENDING' ? '재분석' : 'AI 분석' }}
|
|
</button>
|
|
<button
|
|
v-if="meeting.aiStatus === 'PENDING' && aiResult"
|
|
class="btn btn-sm btn-success"
|
|
@click="confirmAnalysis"
|
|
:disabled="isConfirming"
|
|
>
|
|
<span v-if="isConfirming"><span class="spinner-border spinner-border-sm me-1"></span></span>
|
|
<i v-else class="bi bi-check-lg me-1"></i>확정하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<!-- 분석 결과 없음 -->
|
|
<div v-if="!aiResult" class="text-center text-muted py-4">
|
|
<i class="bi bi-robot display-4"></i>
|
|
<p class="mt-2">AI 분석을 실행해주세요.</p>
|
|
</div>
|
|
|
|
<!-- 분석 결과 표시 -->
|
|
<div v-else>
|
|
<!-- 요약 -->
|
|
<div class="alert alert-info mb-3">
|
|
<i class="bi bi-lightbulb me-2"></i>
|
|
<strong>요약:</strong> {{ aiResult.summary }}
|
|
</div>
|
|
|
|
<!-- 안건 목록 -->
|
|
<div v-for="agenda in aiResult.agendas" :key="agenda.no" class="border rounded p-3 mb-3">
|
|
<h6 class="mb-2">
|
|
<span class="badge bg-secondary me-2">{{ agenda.no }}</span>
|
|
{{ agenda.title }}
|
|
<span :class="getStatusBadge(agenda.status)" class="ms-2">{{ getStatusText(agenda.status) }}</span>
|
|
</h6>
|
|
<p class="text-muted small mb-2">{{ agenda.content }}</p>
|
|
<p v-if="agenda.decision" class="mb-2">
|
|
<i class="bi bi-check-circle text-success me-1"></i>
|
|
<strong>결정:</strong> {{ agenda.decision }}
|
|
</p>
|
|
|
|
<!-- TODO 항목 -->
|
|
<div v-if="agenda.todos && agenda.todos.length > 0" class="mt-2">
|
|
<div v-for="(todo, tIdx) in agenda.todos" :key="tIdx" class="form-check">
|
|
<input
|
|
v-if="meeting.aiStatus === 'PENDING'"
|
|
class="form-check-input"
|
|
type="checkbox"
|
|
:id="`todo-${agenda.no}-${tIdx}`"
|
|
v-model="selectedTodos"
|
|
:value="{ agendaNo: agenda.no, todoIndex: tIdx, title: todo.title, assignee: todo.assignee }"
|
|
/>
|
|
<label class="form-check-label" :for="`todo-${agenda.no}-${tIdx}`">
|
|
<i class="bi bi-check2-square text-primary me-1"></i>
|
|
{{ todo.title }}
|
|
<span v-if="todo.assignee" class="badge bg-light text-dark ms-1">@{{ todo.assignee }}</span>
|
|
</label>
|
|
<small class="d-block text-muted ms-4">{{ todo.reason }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="meeting.aiStatus === 'PENDING'" class="alert alert-warning mb-0">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
TODO로 생성할 항목을 선택한 후 [확정하기]를 클릭하세요.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 직원 선택 모달 -->
|
|
<div class="modal fade" :class="{ show: showEmployeeModal }" :style="{ display: showEmployeeModal ? 'block' : 'none' }">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">내부 참석자 추가</h5>
|
|
<button type="button" class="btn-close" @click="showEmployeeModal = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control mb-3" v-model="employeeSearch" placeholder="이름 검색..." />
|
|
<div style="max-height: 300px; overflow-y: auto;">
|
|
<div v-for="emp in filteredEmployees" :key="emp.employeeId" class="form-check">
|
|
<input class="form-check-input" type="checkbox" :id="`emp-${emp.employeeId}`" :value="emp.employeeId" v-model="selectedEmployeeIds" />
|
|
<label class="form-check-label" :for="`emp-${emp.employeeId}`">
|
|
{{ emp.employeeName }} <small class="text-muted">({{ emp.company }})</small>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="showEmployeeModal = false">취소</button>
|
|
<button type="button" class="btn btn-primary" @click="addSelectedEmployees">추가</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop fade show" v-if="showEmployeeModal"></div>
|
|
|
|
<!-- 외부 참석자 모달 -->
|
|
<div class="modal fade" :class="{ show: showExternalModal }" :style="{ display: showExternalModal ? 'block' : 'none' }">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">외부 참석자 추가</h5>
|
|
<button type="button" class="btn-close" @click="showExternalModal = false"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">이름 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" v-model="externalForm.name" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">소속</label>
|
|
<input type="text" class="form-control" v-model="externalForm.company" />
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="showExternalModal = false">취소</button>
|
|
<button type="button" class="btn btn-primary" @click="confirmExternalAttendee">추가</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop fade show" v-if="showExternalModal"></div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { fetchCurrentUser } = useAuth()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const meetingId = computed(() => Number(route.params.id))
|
|
|
|
const meeting = ref<any>(null)
|
|
const attendees = ref<any[]>([])
|
|
const agendas = ref<any[]>([])
|
|
const projects = ref<any[]>([])
|
|
const employees = ref<any[]>([])
|
|
|
|
const isLoading = ref(true)
|
|
const isEditing = ref(false)
|
|
const isSaving = ref(false)
|
|
|
|
// AI 분석 관련
|
|
const aiResult = ref<any>(null)
|
|
const isAnalyzing = ref(false)
|
|
const isConfirming = ref(false)
|
|
const selectedTodos = ref<any[]>([])
|
|
|
|
const form = ref({
|
|
meetingTitle: '',
|
|
meetingType: 'PROJECT' as 'PROJECT' | 'INTERNAL',
|
|
projectId: '' as string | number,
|
|
meetingDate: '',
|
|
startTime: '',
|
|
endTime: '',
|
|
location: '',
|
|
rawContent: '',
|
|
attendees: [] as any[]
|
|
})
|
|
|
|
const displayAttendees = computed(() => isEditing.value ? form.value.attendees : attendees.value)
|
|
|
|
// 모달
|
|
const showEmployeeModal = ref(false)
|
|
const showExternalModal = ref(false)
|
|
const employeeSearch = ref('')
|
|
const selectedEmployeeIds = ref<number[]>([])
|
|
const externalForm = ref({ name: '', company: '' })
|
|
|
|
const filteredEmployees = computed(() => {
|
|
if (!employeeSearch.value) return employees.value
|
|
return employees.value.filter(e => e.employeeName.toLowerCase().includes(employeeSearch.value.toLowerCase()))
|
|
})
|
|
|
|
onMounted(async () => {
|
|
const user = await fetchCurrentUser()
|
|
if (!user) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
await Promise.all([loadMeeting(), loadProjects(), loadEmployees()])
|
|
})
|
|
|
|
async function loadMeeting() {
|
|
isLoading.value = true
|
|
try {
|
|
const res = await $fetch<any>(`/api/meeting/${meetingId.value}/detail`)
|
|
meeting.value = res.meeting
|
|
attendees.value = res.attendees || []
|
|
agendas.value = res.agendas || []
|
|
|
|
// AI 분석 결과 로드
|
|
if (res.meeting.aiSummary) {
|
|
try {
|
|
aiResult.value = typeof res.meeting.aiSummary === 'string'
|
|
? JSON.parse(res.meeting.aiSummary)
|
|
: res.meeting.aiSummary
|
|
} catch (e) {
|
|
aiResult.value = null
|
|
}
|
|
} else {
|
|
aiResult.value = null
|
|
}
|
|
selectedTodos.value = []
|
|
|
|
// 폼 초기화
|
|
form.value = {
|
|
meetingTitle: res.meeting.meetingTitle,
|
|
meetingType: res.meeting.meetingType,
|
|
projectId: res.meeting.projectId || '',
|
|
meetingDate: res.meeting.meetingDate?.split('T')[0] || '',
|
|
startTime: res.meeting.startTime?.slice(0, 5) || '',
|
|
endTime: res.meeting.endTime?.slice(0, 5) || '',
|
|
location: res.meeting.location || '',
|
|
rawContent: res.meeting.rawContent || '',
|
|
attendees: res.attendees.map((a: any) => ({
|
|
employeeId: a.employeeId,
|
|
employeeName: a.employeeName,
|
|
company: a.company,
|
|
externalName: a.externalName,
|
|
externalCompany: a.externalCompany,
|
|
isExternal: a.isExternal
|
|
}))
|
|
}
|
|
} catch (e) {
|
|
console.error('Load meeting error:', e)
|
|
alert('회의록을 불러올 수 없습니다.')
|
|
router.push('/meeting')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadProjects() {
|
|
try {
|
|
const res = await $fetch<{ projects: any[] }>('/api/project/list')
|
|
projects.value = res.projects || []
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
async function loadEmployees() {
|
|
try {
|
|
const res = await $fetch<{ employees: any[] }>('/api/employee/list')
|
|
employees.value = res.employees || []
|
|
} catch (e) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
function cancelEdit() {
|
|
isEditing.value = false
|
|
// 폼 초기화
|
|
form.value.attendees = attendees.value.map(a => ({ ...a }))
|
|
}
|
|
|
|
function addSelectedEmployees() {
|
|
for (const empId of selectedEmployeeIds.value) {
|
|
if (form.value.attendees.some(a => a.employeeId === empId)) continue
|
|
const emp = employees.value.find(e => e.employeeId === empId)
|
|
if (emp) {
|
|
form.value.attendees.push({
|
|
employeeId: emp.employeeId,
|
|
employeeName: emp.employeeName,
|
|
company: emp.company,
|
|
isExternal: false
|
|
})
|
|
}
|
|
}
|
|
selectedEmployeeIds.value = []
|
|
showEmployeeModal.value = false
|
|
}
|
|
|
|
function addExternalAttendee() {
|
|
externalForm.value = { name: '', company: '' }
|
|
showExternalModal.value = true
|
|
}
|
|
|
|
function confirmExternalAttendee() {
|
|
if (!externalForm.value.name) {
|
|
alert('이름을 입력하세요.')
|
|
return
|
|
}
|
|
form.value.attendees.push({
|
|
externalName: externalForm.value.name,
|
|
externalCompany: externalForm.value.company,
|
|
isExternal: true
|
|
})
|
|
showExternalModal.value = false
|
|
}
|
|
|
|
function removeAttendee(idx: number) {
|
|
form.value.attendees.splice(idx, 1)
|
|
}
|
|
|
|
async function updateMeeting() {
|
|
if (!form.value.meetingTitle) {
|
|
alert('회의 제목을 입력하세요.')
|
|
return
|
|
}
|
|
|
|
isSaving.value = true
|
|
try {
|
|
await $fetch(`/api/meeting/${meetingId.value}/update`, {
|
|
method: 'PUT',
|
|
body: {
|
|
meetingTitle: form.value.meetingTitle,
|
|
meetingType: form.value.meetingType,
|
|
projectId: form.value.projectId || undefined,
|
|
meetingDate: form.value.meetingDate,
|
|
startTime: form.value.startTime || undefined,
|
|
endTime: form.value.endTime || undefined,
|
|
location: form.value.location || undefined,
|
|
rawContent: form.value.rawContent || undefined,
|
|
attendees: form.value.attendees.map(a => ({
|
|
employeeId: a.employeeId,
|
|
externalName: a.externalName,
|
|
externalCompany: a.externalCompany
|
|
}))
|
|
}
|
|
})
|
|
|
|
isEditing.value = false
|
|
await loadMeeting()
|
|
} catch (e: any) {
|
|
alert(e.data?.message || e.message || '수정에 실패했습니다.')
|
|
} finally {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
async function deleteMeeting() {
|
|
if (!confirm('정말 삭제하시겠습니까?')) return
|
|
|
|
try {
|
|
await $fetch(`/api/meeting/${meetingId.value}/delete`, { method: 'DELETE' })
|
|
router.push('/meeting')
|
|
} catch (e: any) {
|
|
alert(e.data?.message || e.message || '삭제에 실패했습니다.')
|
|
}
|
|
}
|
|
|
|
function formatDate(dateStr: string) {
|
|
if (!dateStr) return ''
|
|
const d = new Date(dateStr)
|
|
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
|
}
|
|
|
|
// AI 분석
|
|
async function analyzeContent() {
|
|
if (!meeting.value?.rawContent) {
|
|
alert('분석할 회의 내용이 없습니다.')
|
|
return
|
|
}
|
|
|
|
isAnalyzing.value = true
|
|
try {
|
|
const res = await $fetch<{ result: any }>(`/api/meeting/${meetingId.value}/analyze`, { method: 'POST' })
|
|
aiResult.value = res.result
|
|
meeting.value.aiStatus = 'PENDING'
|
|
selectedTodos.value = []
|
|
alert('AI 분석이 완료되었습니다.')
|
|
} catch (e: any) {
|
|
alert(e.data?.message || 'AI 분석에 실패했습니다.')
|
|
} finally {
|
|
isAnalyzing.value = false
|
|
}
|
|
}
|
|
|
|
async function confirmAnalysis() {
|
|
if (selectedTodos.value.length === 0) {
|
|
if (!confirm('선택된 TODO가 없습니다. 그래도 확정하시겠습니까?')) return
|
|
}
|
|
|
|
isConfirming.value = true
|
|
try {
|
|
const res = await $fetch<{ message: string }>(`/api/meeting/${meetingId.value}/confirm`, {
|
|
method: 'POST',
|
|
body: { selectedTodos: selectedTodos.value }
|
|
})
|
|
meeting.value.aiStatus = 'CONFIRMED'
|
|
alert(res.message)
|
|
} catch (e: any) {
|
|
alert(e.data?.message || '확정에 실패했습니다.')
|
|
} finally {
|
|
isConfirming.value = false
|
|
}
|
|
}
|
|
|
|
function getStatusBadge(status: string) {
|
|
return {
|
|
'DECIDED': 'badge bg-success',
|
|
'PENDING': 'badge bg-warning',
|
|
'IN_PROGRESS': 'badge bg-info'
|
|
}[status] || 'badge bg-secondary'
|
|
}
|
|
|
|
function getStatusText(status: string) {
|
|
return {
|
|
'DECIDED': '결정됨',
|
|
'PENDING': '미결정',
|
|
'IN_PROGRESS': '진행중'
|
|
}[status] || status
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.modal.show {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
}
|
|
</style>
|