기능구현중

This commit is contained in:
2026-01-11 13:47:33 +09:00
parent f5bf084afc
commit d56154d5d2
13 changed files with 948 additions and 46 deletions

View File

@@ -138,9 +138,10 @@
</div>
</div>
<!-- 오른쪽: 회의 내용 -->
<!-- 오른쪽: 회의 내용 + AI 분석 -->
<div class="col-md-8">
<div class="card h-100">
<!-- 회의 내용 -->
<div class="card mb-4">
<div class="card-header">
<strong>회의 내용</strong>
</div>
@@ -149,9 +150,9 @@
v-if="isEditing"
class="form-control border-0 h-100"
v-model="form.rawContent"
style="min-height: 500px; resize: none;"
style="min-height: 300px; resize: none;"
></textarea>
<div v-else class="p-3" style="min-height: 500px; white-space: pre-wrap;">{{ meeting.rawContent || '(내용 없음)' }}</div>
<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>
@@ -163,6 +164,94 @@
</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>
@@ -241,6 +330,12 @@ 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',
@@ -285,6 +380,20 @@ async function loadMeeting() {
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,
@@ -428,6 +537,63 @@ function formatDate(dateStr: string) {
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>