Chore: 회의 종료 API 실제 데이터 연동

This commit is contained in:
cyjadela
2025-10-27 15:46:47 +09:00
parent 6a2574e9f5
commit c16e3e8fd4
3 changed files with 529 additions and 142 deletions
@@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@@ -327,9 +328,12 @@ public class MeetingService implements
log.debug("Found minutes: {}", minutes.getTitle());
}
// 5. AI 분석 수행 (현재는 Mock 데이터로 구현)
MeetingAnalysis analysis = performAIAnalysis(meeting, minutes);
// 5. 기본 분석 정보 생성 (실제 AI 분석은 AI 서비스에서 비동기 처리)
MeetingAnalysis analysis = createBasicAnalysis(meeting, minutes);
meetingAnalysisWriter.save(analysis);
// TODO: AI 서비스에 비동기 분석 요청 전송
// aiAnalysisClient.requestAnalysis(meeting.getMeetingId(), minutes.getMinutesId());
// 6. 결과 DTO 구성
MeetingEndDTO result = buildMeetingEndDTO(meeting, analysis);
@@ -339,55 +343,20 @@ public class MeetingService implements
}
/**
* AI 분석 수행 (Mock 구현)
* 간단한 회의 분석 수행 (AI 서비스 없이 기본 정보만 처리)
* AI 분석은 별도 AI 서비스에서 비동기로 처리되어야 함
*/
private MeetingAnalysis performAIAnalysis(Meeting meeting, Minutes minutes) {
log.info("Performing AI analysis for meeting: {}", meeting.getMeetingId());
// Mock 데이터로 구현 (실제로는 AI 서비스 호출)
List<String> keywords = List.of(
"#신제품기획", "#예산편성", "#일정조율",
"#시장조사", "#UI/UX", "#개발스펙"
);
List<MeetingAnalysis.AgendaAnalysis> agendaAnalyses = List.of(
MeetingAnalysis.AgendaAnalysis.builder()
.agendaId("agenda-1")
.title("1. 신제품 기획 방향성")
.aiSummaryShort("타겟 고객을 20-30대로 설정, UI/UX 개선 집중")
.discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고, 기존 제품 대비 UI/UX를 대폭 개선하기로 함")
.decisions(List.of("타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정"))
.pending(List.of())
.extractedTodos(List.of("시장 조사 보고서 작성", "UI/UX 개선안 초안 작성"))
.build(),
MeetingAnalysis.AgendaAnalysis.builder()
.agendaId("agenda-2")
.title("2. 예산 편성 및 일정")
.aiSummaryShort("총 예산 5억, 개발 기간 6개월 확정")
.discussion("신제품 개발을 위한 총 예산을 5억원으로 책정하고, 개발 기간은 6개월로 확정함")
.decisions(List.of("총 예산: 5억원", "개발 기간: 6개월", "예산 배분: 개발 60%, 마케팅 40%"))
.pending(List.of("세부 일정 확정은 다음 회의에서 논의"))
.extractedTodos(List.of("세부 개발 일정 수립"))
.build(),
MeetingAnalysis.AgendaAnalysis.builder()
.agendaId("agenda-3")
.title("3. 기술 스택 및 개발 방향")
.aiSummaryShort("React 기반 프론트엔드, AI 챗봇 기능 추가")
.discussion("프론트엔드는 React 기반으로 개발하고, 고객 지원을 위한 AI 챗봇 기능을 추가하기로 함")
.decisions(List.of("프론트엔드: React 기반", "AI 챗봇 기능 추가", "Next.js 도입 검토"))
.pending(List.of("AI 챗봇 학습 데이터 확보 방안"))
.extractedTodos(List.of("AI 챗봇 프로토타입 개발", "Next.js 도입 검토 보고서"))
.build()
);
private MeetingAnalysis createBasicAnalysis(Meeting meeting, Minutes minutes) {
log.info("Creating basic analysis for meeting: {}", meeting.getMeetingId());
// 기본 분석 정보만 생성 (키워드나 상세 분석은 AI 서비스에서 별도 처리)
return MeetingAnalysis.builder()
.analysisId(UUID.randomUUID().toString())
.meetingId(meeting.getMeetingId())
.minutesId(minutes.getMinutesId())
.keywords(keywords)
.agendaAnalyses(agendaAnalyses)
.status("COMPLETED")
.completedAt(LocalDateTime.now())
.keywords(List.of()) // AI 서비스에서 별도로 채워짐
.agendaAnalyses(List.of()) // AI 서비스에서 별도로 채워짐
.status("PENDING") // AI 처리 대기 상태
.createdAt(LocalDateTime.now())
.build();
}
@@ -396,10 +365,11 @@ public class MeetingService implements
* MeetingEndDTO 구성
*/
private MeetingEndDTO buildMeetingEndDTO(Meeting meeting, MeetingAnalysis analysis) {
// 회의 시간 계산 (Mock)
int durationMinutes = 90;
// 회의 시간 및 참석자 수 계산 (실제 데이터 기반)
int durationMinutes = calculateActualDuration(meeting);
int participantCount = calculateActualParticipantCount(meeting.getMeetingId());
// 전체 Todo 개수 계산
// 전체 Todo 개수 계산 (AI 분석 완료되기 전까지는 0)
int totalTodos = analysis.getAgendaAnalyses().stream()
.mapToInt(agenda -> agenda.getExtractedTodos().size())
.sum();
@@ -424,7 +394,7 @@ public class MeetingService implements
return MeetingEndDTO.builder()
.title(meeting.getTitle())
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
.participantCount(participantCount)
.durationMinutes(durationMinutes)
.agendaCount(analysis.getAgendaAnalyses().size())
.todoCount(totalTodos)
@@ -460,6 +430,36 @@ public class MeetingService implements
return updatedMeeting;
}
/**
* 회의 실제 진행 시간 계산
*/
private int calculateActualDuration(Meeting meeting) {
if (meeting.getStartedAt() != null && meeting.getEndedAt() != null) {
// 실제 시작/종료 시간이 있으면 실제 진행 시간 계산
Duration duration = Duration.between(meeting.getStartedAt(), meeting.getEndedAt());
return (int) duration.toMinutes();
} else if (meeting.getScheduledAt() != null && meeting.getEndTime() != null) {
// 예정 시간 기준으로 계산
Duration duration = Duration.between(meeting.getScheduledAt(), meeting.getEndTime());
return (int) duration.toMinutes();
} else {
// 기본값 (1시간)
return 60;
}
}
/**
* 실제 참석자 수 계산
*/
private int calculateActualParticipantCount(String meetingId) {
// 실제 참석한 참석자 수 계산 (meeting_participants 테이블에서 attended=true인 수)
// 현재는 기본적으로 주최자 + 참석자 수로 계산
List<String> participants = meetingReader.findById(meetingId)
.map(Meeting::getParticipants)
.orElse(List.of());
return participants.size() + 1; // 주최자 포함
}
/**
* ID로 회의 조회
*/
@@ -1,7 +1,10 @@
package com.unicorn.hgzero.meeting.infra.controller;
import com.unicorn.hgzero.common.dto.ApiResponse;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.dto.MeetingDTO;
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
import com.unicorn.hgzero.meeting.infra.dto.request.CreateMeetingRequest;
import com.unicorn.hgzero.meeting.infra.dto.request.InviteParticipantRequest;
@@ -191,101 +194,25 @@ public class MeetingController {
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
// Mock 데이터로 응답 (개발용)
var response = createMockMeetingEndResponse(meetingId);
log.info("회의 종료 완료 (Mock) - meetingId: {}", meetingId);
return ResponseEntity.ok(ApiResponse.success(response));
try {
// 실제 비즈니스 로직 호출
MeetingEndDTO meetingEndDTO = endMeetingUseCase.endMeeting(meetingId);
// DTO를 응답 객체로 변환
MeetingEndResponse response = MeetingEndResponse.from(meetingEndDTO);
log.info("회의 종료 완료 - meetingId: {}", meetingId);
return ResponseEntity.ok(ApiResponse.success(response));
} catch (BusinessException e) {
log.error("회의 종료 실패 - meetingId: {}, error: {}", meetingId, e.getMessage());
throw e;
} catch (Exception e) {
log.error("회의 종료 중 예상치 못한 오류 - meetingId: {}", meetingId, e);
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR, "회의 종료 처리 중 오류가 발생했습니다.");
}
}
/**
* 회의 종료 응답 Mock 데이터 생성
*
* @param meetingId 회의 ID
* @return Mock 회의 종료 응답
*/
private MeetingEndResponse createMockMeetingEndResponse(String meetingId) {
return MeetingEndResponse.builder()
.title("Q1 전략 기획 회의")
.participantCount(4)
.durationMinutes(90)
.agendaCount(3)
.todoCount(5)
.keywords(List.of("신제품 기획", "마케팅 전략", "예산 계획", "UI/UX 개선", "고객 분석"))
.agendaSummaries(List.of(
MeetingEndResponse.AgendaSummary.builder()
.title("1. 신제품 기획 방향성")
.aiSummaryShort("타겟 고객을 20-30대로 설정하고 UI/UX 개선에 집중하기로 결정")
.details(MeetingEndResponse.AgendaDetails.builder()
.discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고 모바일 중심의 사용자 경험을 강화하는 방향으로 논의됨")
.decisions(List.of(
"타겟 고객: 20-30대 직장인",
"플랫폼: 모바일 우선",
"핵심 기능: 간편 결제, 개인화 추천"
))
.pending(List.of(
"경쟁사 분석 보완 필요",
"기술 스택 최종 검토"
))
.build())
.todos(List.of(
MeetingEndResponse.TodoSummary.builder()
.title("시장 조사 보고서 작성")
.build(),
MeetingEndResponse.TodoSummary.builder()
.title("와이어프레임 초안 제작")
.build()
))
.build(),
MeetingEndResponse.AgendaSummary.builder()
.title("2. 마케팅 전략 수립")
.aiSummaryShort("SNS 마케팅과 인플루언서 협업을 통한 브랜드 인지도 향상 계획")
.details(MeetingEndResponse.AgendaDetails.builder()
.discussion("초기 론칭 시 SNS 중심의 마케팅 전략과 마이크로 인플루언서 협업을 통한 브랜드 인지도 향상 방안 논의")
.decisions(List.of(
"마케팅 채널: 인스타그램, 틱톡 우선",
"예산 배분: 인플루언서 50%, 광고 30%, 이벤트 20%",
"론칭 시기: 2024년 2분기"
))
.pending(List.of(
"인플루언서 리스트 검토",
"마케팅 예산 최종 승인"
))
.build())
.todos(List.of(
MeetingEndResponse.TodoSummary.builder()
.title("인플루언서 후보 리스트 작성")
.build(),
MeetingEndResponse.TodoSummary.builder()
.title("마케팅 예산안 상세 작성")
.build()
))
.build(),
MeetingEndResponse.AgendaSummary.builder()
.title("3. 프로젝트 일정 및 리소스")
.aiSummaryShort("개발 6개월, 테스트 2개월로 총 8개월 일정 확정")
.details(MeetingEndResponse.AgendaDetails.builder()
.discussion("전체 프로젝트 일정을 8개월로 설정하고 개발팀 6명, 디자인팀 2명으로 팀 구성 확정")
.decisions(List.of(
"전체 일정: 8개월 (개발 6개월, 테스트 2개월)",
"팀 구성: 개발 6명, 디자인 2명, PM 1명",
"주요 마일스톤: MVP 3개월, 베타 6개월, 정식 출시 8개월"
))
.pending(List.of(
"개발자 추가 채용 검토",
"외부 업체 협업 범위 논의"
))
.build())
.todos(List.of(
MeetingEndResponse.TodoSummary.builder()
.title("개발자 채용 공고 작성")
.build()
))
.build()
))
.build();
}
/**
* 회의 정보 조회