mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-12 22:59:10 +00:00
feat: Meeting AI 통합 - 회의 종료 API 및 AI 회의록 요약 기능 구현
주요 변경사항:
- 회의 종료 API 구현 (POST /api/meetings/{meetingId}/end)
- AI 회의록 통합 요약 기능 구현
- Claude API 연동 및 프롬프트 최적화
- 안건별 요약, 키워드 추출, 결정사항 자동 정리
AI Service (Python):
- Claude 모델 설정: claude-sonnet-4-5-20250929
- 회의록 통합 프롬프트 개선
- AgendaSummary 모델 summary 필드 매핑 수정
- decisions 필드 추가 및 응답 구조 정리
- 입력 데이터 로깅 추가
Meeting Service (Java):
- EndMeetingService AI 통합 로직 구현
- MeetingAnalysis 엔티티 decisions 필드 추가
- AgendaSection opinions 필드 제거
- AI Service 포트 8086으로 설정
- DB 마이그레이션 스크립트 추가 (V7)
테스트 결과:
✅ 회의 종료 API 정상 동작
✅ AI 응답 검증 (keywords, summary, decisions)
✅ 안건별 요약 및 보류사항 추출
✅ 처리 시간: ~11초, 토큰: ~2,600
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -49,25 +49,17 @@ public class AgendaSection {
|
||||
private String aiSummaryShort;
|
||||
|
||||
/**
|
||||
* 논의 사항 (핵심 내용 3-5문장)
|
||||
* 안건별 회의록 요약
|
||||
* 사용자가 입력한 회의록 내용을 요약한 결과
|
||||
* (기존 discussions, decisions, opinions를 통합)
|
||||
*/
|
||||
private String discussions;
|
||||
|
||||
/**
|
||||
* 결정 사항 목록
|
||||
*/
|
||||
private List<String> decisions;
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 보류 사항 목록
|
||||
*/
|
||||
private List<String> pendingItems;
|
||||
|
||||
/**
|
||||
* 참석자별 의견
|
||||
*/
|
||||
private List<ParticipantOpinion> opinions;
|
||||
|
||||
/**
|
||||
* AI 추출 Todo 목록
|
||||
*/
|
||||
@@ -83,25 +75,6 @@ public class AgendaSection {
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 참석자 의견 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ParticipantOpinion {
|
||||
/**
|
||||
* 발언자 이름
|
||||
*/
|
||||
private String speaker;
|
||||
|
||||
/**
|
||||
* 의견 내용
|
||||
*/
|
||||
private String opinion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 항목 내부 클래스
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,11 @@ public class MeetingAnalysis {
|
||||
*/
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 안건별 분석 결과
|
||||
*/
|
||||
|
||||
@@ -76,6 +76,11 @@ public class Minutes {
|
||||
*/
|
||||
private String lastModifiedBy;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 확정자 ID
|
||||
*/
|
||||
|
||||
@@ -106,6 +106,11 @@ public class MinutesDTO {
|
||||
*/
|
||||
private final LocalDateTime lastModifiedAt;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private final String decisions;
|
||||
|
||||
/**
|
||||
* Todo 개수
|
||||
*/
|
||||
|
||||
@@ -140,8 +140,8 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
.agendaId(UUID.randomUUID().toString())
|
||||
.title(summary.getAgendaTitle())
|
||||
.aiSummaryShort(summary.getSummaryShort())
|
||||
.discussion(summary.getDiscussion() != null ? summary.getDiscussion() : "")
|
||||
.decisions(summary.getDecisions() != null ? summary.getDecisions() : List.of())
|
||||
.discussion(summary.getSummary() != null ? summary.getSummary() : "")
|
||||
.decisions(List.of())
|
||||
.pending(summary.getPending() != null ? summary.getPending() : List.of())
|
||||
.extractedTodos(summary.getTodos() != null
|
||||
? summary.getTodos().stream()
|
||||
@@ -157,6 +157,7 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.minutesId(meeting.getMeetingId()) // 실제로는 minutesId 필요
|
||||
.keywords(aiResponse.getKeywords())
|
||||
.decisions(aiResponse.getDecisions())
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status("COMPLETED")
|
||||
.completedAt(LocalDateTime.now())
|
||||
|
||||
+1
-2
@@ -181,8 +181,7 @@ public class MeetingAiController {
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.discussions(section.getDiscussions())
|
||||
.decisions(section.getDecisions())
|
||||
.summary(section.getSummary())
|
||||
.pendingItems(section.getPendingItems())
|
||||
.todos(todos)
|
||||
.createdAt(section.getCreatedAt())
|
||||
|
||||
@@ -36,14 +36,10 @@ public class AgendaSummaryDTO {
|
||||
private String summaryShort;
|
||||
|
||||
/**
|
||||
* 논의 주제
|
||||
* 안건별 회의록 요약
|
||||
* 사용자가 입력한 회의록 내용을 요약한 결과
|
||||
*/
|
||||
private String discussion;
|
||||
|
||||
/**
|
||||
* 결정 사항
|
||||
*/
|
||||
private List<String> decisions;
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 보류 사항
|
||||
|
||||
@@ -39,6 +39,11 @@ public class ConsolidateResponse {
|
||||
*/
|
||||
private Map<String, Integer> statistics;
|
||||
|
||||
/**
|
||||
* 회의 전체 결정사항
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 안건별 요약
|
||||
*/
|
||||
|
||||
+3
@@ -1,5 +1,6 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.ai;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
@@ -17,11 +18,13 @@ public class ParticipantMinutesDTO {
|
||||
/**
|
||||
* 사용자 ID
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 사용자 이름
|
||||
*/
|
||||
@JsonProperty("user_name")
|
||||
private String userName;
|
||||
|
||||
/**
|
||||
|
||||
+2
-5
@@ -49,11 +49,8 @@ public class AgendaSectionResponse {
|
||||
@Schema(description = "AI 생성 짧은 요약", example = "타겟 고객을 20-30대로 설정")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Schema(description = "논의 사항", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
|
||||
private String discussions;
|
||||
|
||||
@Schema(description = "결정 사항 목록")
|
||||
private List<String> decisions;
|
||||
@Schema(description = "안건별 회의록 요약", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "보류 사항 목록")
|
||||
private List<String> pendingItems;
|
||||
|
||||
+4
-14
@@ -47,18 +47,12 @@ public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
@Column(name = "ai_summary_short", columnDefinition = "TEXT")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Column(name = "discussions", columnDefinition = "TEXT")
|
||||
private String discussions;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "json")
|
||||
private String decisionsJson;
|
||||
@Column(name = "summary", columnDefinition = "TEXT")
|
||||
private String summary;
|
||||
|
||||
@Column(name = "pending_items", columnDefinition = "json")
|
||||
private String pendingItemsJson;
|
||||
|
||||
@Column(name = "opinions", columnDefinition = "json")
|
||||
private String opinionsJson;
|
||||
|
||||
@Column(name = "todos", columnDefinition = "json")
|
||||
private String todosJson;
|
||||
|
||||
@@ -73,10 +67,8 @@ public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
.agendaNumber(this.agendaNumber)
|
||||
.agendaTitle(this.agendaTitle)
|
||||
.aiSummaryShort(this.aiSummaryShort)
|
||||
.discussions(this.discussions)
|
||||
.decisions(parseJsonToList(this.decisionsJson, new TypeReference<List<String>>() {}))
|
||||
.summary(this.summary)
|
||||
.pendingItems(parseJsonToList(this.pendingItemsJson, new TypeReference<List<String>>() {}))
|
||||
.opinions(parseJsonToList(this.opinionsJson, new TypeReference<List<AgendaSection.ParticipantOpinion>>() {}))
|
||||
.todos(parseJsonToList(this.todosJson, new TypeReference<List<AgendaSection.TodoItem>>() {}))
|
||||
.createdAt(this.getCreatedAt())
|
||||
.updatedAt(this.getUpdatedAt())
|
||||
@@ -94,10 +86,8 @@ public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.discussions(section.getDiscussions())
|
||||
.decisionsJson(toJson(section.getDecisions()))
|
||||
.summary(section.getSummary())
|
||||
.pendingItemsJson(toJson(section.getPendingItems()))
|
||||
.opinionsJson(toJson(section.getOpinions()))
|
||||
.todosJson(toJson(section.getTodos()))
|
||||
.build();
|
||||
}
|
||||
|
||||
+7
-2
@@ -38,6 +38,9 @@ public class MeetingAnalysisEntity {
|
||||
@Column(name = "keyword")
|
||||
private List<String> keywords;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "agenda_analyses", columnDefinition = "TEXT")
|
||||
private String agendaAnalysesJson; // JSON 문자열로 저장
|
||||
|
||||
@@ -55,12 +58,13 @@ public class MeetingAnalysisEntity {
|
||||
*/
|
||||
public MeetingAnalysis toDomain() {
|
||||
List<MeetingAnalysis.AgendaAnalysis> agendaAnalyses = parseAgendaAnalyses();
|
||||
|
||||
|
||||
return MeetingAnalysis.builder()
|
||||
.analysisId(this.analysisId)
|
||||
.meetingId(this.meetingId)
|
||||
.minutesId(this.minutesId)
|
||||
.keywords(this.keywords)
|
||||
.decisions(this.decisions)
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status(this.status)
|
||||
.completedAt(this.completedAt)
|
||||
@@ -90,12 +94,13 @@ public class MeetingAnalysisEntity {
|
||||
*/
|
||||
public static MeetingAnalysisEntity fromDomain(MeetingAnalysis domain) {
|
||||
String agendaAnalysesJson = convertAgendaAnalysesToJson(domain.getAgendaAnalyses());
|
||||
|
||||
|
||||
return MeetingAnalysisEntity.builder()
|
||||
.analysisId(domain.getAnalysisId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.minutesId(domain.getMinutesId())
|
||||
.keywords(domain.getKeywords())
|
||||
.decisions(domain.getDecisions())
|
||||
.agendaAnalysesJson(agendaAnalysesJson)
|
||||
.status(domain.getStatus())
|
||||
.completedAt(domain.getCompletedAt())
|
||||
|
||||
+5
@@ -48,6 +48,9 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "created_by", length = 50, nullable = false)
|
||||
private String createdBy;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "finalized_by", length = 50)
|
||||
private String finalizedBy;
|
||||
|
||||
@@ -66,6 +69,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.createdBy(this.createdBy)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.lastModifiedAt(this.getUpdatedAt())
|
||||
.decisions(this.decisions)
|
||||
.finalizedBy(this.finalizedBy)
|
||||
.finalizedAt(this.finalizedAt)
|
||||
.build();
|
||||
@@ -80,6 +84,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
.decisions(minutes.getDecisions())
|
||||
.finalizedBy(minutes.getFinalizedBy())
|
||||
.finalizedAt(minutes.getFinalizedAt())
|
||||
.build();
|
||||
|
||||
@@ -137,5 +137,5 @@ azure:
|
||||
# AI Service Configuration
|
||||
ai:
|
||||
service:
|
||||
url: ${AI_SERVICE_URL:http://localhost:8087}
|
||||
url: ${AI_SERVICE_URL:http://localhost:8086}
|
||||
timeout: ${AI_SERVICE_TIMEOUT:30000}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
-- ========================================
|
||||
-- V7: minutes 테이블에 decisions 추가 및 agenda_sections 리팩토링
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-29
|
||||
-- 설명:
|
||||
-- 1. minutes 테이블에 decisions (결정사항) 컬럼 추가
|
||||
-- 2. agenda_sections 테이블에서 discussions, decisions, opinions를 summary로 통합
|
||||
|
||||
-- ========================================
|
||||
-- 1. minutes 테이블에 decisions 컬럼 추가
|
||||
-- ========================================
|
||||
-- 회의 전체 결정사항을 TEXT 형식으로 저장
|
||||
|
||||
ALTER TABLE minutes
|
||||
ADD COLUMN IF NOT EXISTS decisions TEXT;
|
||||
|
||||
COMMENT ON COLUMN minutes.decisions IS '회의 전체 결정사항 (회의록 수정 시 입력)';
|
||||
|
||||
-- ========================================
|
||||
-- 2. agenda_sections 테이블 백업 및 데이터 마이그레이션
|
||||
-- ========================================
|
||||
-- 기존 데이터 보존을 위한 임시 백업 테이블 생성
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agenda_sections_backup AS
|
||||
SELECT * FROM agenda_sections;
|
||||
|
||||
-- ========================================
|
||||
-- 3. agenda_sections 테이블 컬럼 변경
|
||||
-- ========================================
|
||||
-- discussions, decisions, opinions → summary 통합
|
||||
|
||||
-- summary 컬럼 추가 (기존 discussions 내용으로 초기화)
|
||||
ALTER TABLE agenda_sections
|
||||
ADD COLUMN IF NOT EXISTS summary TEXT;
|
||||
|
||||
-- 기존 데이터 마이그레이션: discussions 내용을 summary로 복사
|
||||
UPDATE agenda_sections
|
||||
SET summary = COALESCE(discussions, '');
|
||||
|
||||
-- 기존 컬럼 삭제
|
||||
ALTER TABLE agenda_sections
|
||||
DROP COLUMN IF EXISTS discussions,
|
||||
DROP COLUMN IF EXISTS decisions,
|
||||
DROP COLUMN IF EXISTS opinions;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN agenda_sections.summary IS '안건별 회의록 요약 (사용자가 입력한 회의록 내용을 AI가 요약한 결과)';
|
||||
|
||||
-- ========================================
|
||||
-- 4. 인덱스 및 트리거 유지
|
||||
-- ========================================
|
||||
-- 기존 인덱스 및 트리거는 그대로 유지됨
|
||||
|
||||
-- ========================================
|
||||
-- 5. 백업 테이블 정리 안내
|
||||
-- ========================================
|
||||
-- agenda_sections_backup 테이블은 수동으로 검증 후 삭제
|
||||
COMMENT ON TABLE agenda_sections_backup IS 'V7 마이그레이션 백업 - 검증 후 수동 삭제 필요';
|
||||
Reference in New Issue
Block a user