mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 20:46:23 +00:00
Chore: 회의 종료 API 수정
This commit is contained in:
parent
ba32a70ad2
commit
fca069cf9c
File diff suppressed because it is too large
Load Diff
BIN
meeting/logs/meeting-service.log.2025-10-24.0.gz
Normal file
BIN
meeting/logs/meeting-service.log.2025-10-24.0.gz
Normal file
Binary file not shown.
@ -0,0 +1,122 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 AI 분석 결과 도메인 모델
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MeetingAnalysis {
|
||||
|
||||
/**
|
||||
* 분석 ID
|
||||
*/
|
||||
private String analysisId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 주요 키워드
|
||||
*/
|
||||
private List<String> keywords;
|
||||
|
||||
/**
|
||||
* 안건별 분석 결과
|
||||
*/
|
||||
private List<AgendaAnalysis> agendaAnalyses;
|
||||
|
||||
/**
|
||||
* 전체 회의 분석 상태 (PENDING, IN_PROGRESS, COMPLETED, FAILED)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 분석 완료 시간
|
||||
*/
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* 생성 시간
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 분석 완료 처리
|
||||
*/
|
||||
public void complete() {
|
||||
this.status = "COMPLETED";
|
||||
this.completedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 분석 실패 처리
|
||||
*/
|
||||
public void fail() {
|
||||
this.status = "FAILED";
|
||||
}
|
||||
|
||||
/**
|
||||
* 분석 진행 중 처리
|
||||
*/
|
||||
public void startAnalysis() {
|
||||
this.status = "IN_PROGRESS";
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AgendaAnalysis {
|
||||
/**
|
||||
* 안건 ID
|
||||
*/
|
||||
private String agendaId;
|
||||
|
||||
/**
|
||||
* 안건 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* AI 요약 (간략)
|
||||
*/
|
||||
private String aiSummaryShort;
|
||||
|
||||
/**
|
||||
* 논의 주제
|
||||
*/
|
||||
private String discussion;
|
||||
|
||||
/**
|
||||
* 결정 사항
|
||||
*/
|
||||
private List<String> decisions;
|
||||
|
||||
/**
|
||||
* 보류 사항
|
||||
*/
|
||||
private List<String> pending;
|
||||
|
||||
/**
|
||||
* 추출된 Todo 목록
|
||||
*/
|
||||
private List<String> extractedTodos;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.meeting.biz.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 종료 비즈니스 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class MeetingEndDTO {
|
||||
|
||||
private final String title;
|
||||
private final int participantCount;
|
||||
private final int durationMinutes;
|
||||
private final int agendaCount;
|
||||
private final int todoCount;
|
||||
private final List<String> keywords;
|
||||
private final List<AgendaSummaryDTO> agendaSummaries;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class AgendaSummaryDTO {
|
||||
private final String title;
|
||||
private final String aiSummaryShort;
|
||||
private final AgendaDetailsDTO details;
|
||||
private final List<TodoSummaryDTO> todos;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class AgendaDetailsDTO {
|
||||
private final String discussion;
|
||||
private final List<String> decisions;
|
||||
private final List<String> pending;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public static class TodoSummaryDTO {
|
||||
private final String title;
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,16 @@ package com.unicorn.hgzero.meeting.biz.service;
|
||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Session;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisWriter;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
|
||||
@ -42,7 +47,10 @@ public class MeetingService implements
|
||||
private final MeetingWriter meetingWriter;
|
||||
private final SessionReader sessionReader;
|
||||
private final SessionWriter sessionWriter;
|
||||
private final MinutesReader minutesReader;
|
||||
private final MinutesWriter minutesWriter;
|
||||
private final MeetingAnalysisReader meetingAnalysisReader;
|
||||
private final MeetingAnalysisWriter meetingAnalysisWriter;
|
||||
private final CacheService cacheService;
|
||||
private final EventPublisher eventPublisher;
|
||||
|
||||
@ -255,30 +263,165 @@ public class MeetingService implements
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료
|
||||
* 회의 종료 및 AI 분석
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public Meeting endMeeting(String meetingId) {
|
||||
public MeetingEndDTO endMeeting(String meetingId) {
|
||||
log.info("Ending meeting: {}", meetingId);
|
||||
|
||||
// 회의 조회
|
||||
// 1. 회의 조회
|
||||
log.debug("Searching for meeting with ID: {}", meetingId);
|
||||
Meeting meeting = meetingReader.findById(meetingId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
|
||||
.orElseThrow(() -> {
|
||||
log.error("Meeting not found: {}", meetingId);
|
||||
return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의를 찾을 수 없습니다: " + meetingId);
|
||||
});
|
||||
log.debug("Found meeting: {}, status: {}", meeting.getTitle(), meeting.getStatus());
|
||||
|
||||
// 회의 상태 검증
|
||||
if (!"IN_PROGRESS".equals(meeting.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
||||
// 2. 회의 상태 검증 (SCHEDULED 또는 IN_PROGRESS만 종료 가능)
|
||||
if (!"SCHEDULED".equals(meeting.getStatus()) && !"IN_PROGRESS".equals(meeting.getStatus())) {
|
||||
log.warn("Invalid meeting status for ending: meetingId={}, status={}", meetingId, meeting.getStatus());
|
||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE,
|
||||
"회의를 종료할 수 없는 상태입니다. 현재 상태: " + meeting.getStatus());
|
||||
}
|
||||
|
||||
// 회의 종료
|
||||
// 3. 회의 종료
|
||||
meeting.end();
|
||||
|
||||
// 저장
|
||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||
|
||||
// 4. 회의록 조회 (SCHEDULED 상태면 빈 회의록 생성, IN_PROGRESS면 기존 회의록 조회)
|
||||
Minutes minutes;
|
||||
if ("SCHEDULED".equals(meeting.getStatus())) {
|
||||
// SCHEDULED 상태에서 종료하는 경우 빈 회의록 생성
|
||||
String minutesId = UUID.randomUUID().toString();
|
||||
minutes = Minutes.builder()
|
||||
.minutesId(minutesId)
|
||||
.meetingId(meetingId)
|
||||
.title(meeting.getTitle() + " - 회의록")
|
||||
.sections(List.of())
|
||||
.status("DRAFT")
|
||||
.version(1)
|
||||
.createdBy(meeting.getOrganizerId())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
minutesWriter.save(minutes);
|
||||
log.info("Empty minutes created for SCHEDULED meeting: meetingId={}, minutesId={}", meetingId, minutesId);
|
||||
} else {
|
||||
// IN_PROGRESS 상태면 기존 회의록 조회
|
||||
log.debug("Searching for existing minutes for meeting: {}", meetingId);
|
||||
minutes = minutesReader.findLatestByMeetingId(meetingId)
|
||||
.orElseThrow(() -> {
|
||||
log.error("Minutes not found for meeting: {}", meetingId);
|
||||
return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의록을 찾을 수 없습니다: " + meetingId);
|
||||
});
|
||||
log.debug("Found minutes: {}", minutes.getTitle());
|
||||
}
|
||||
|
||||
// 5. AI 분석 수행 (현재는 Mock 데이터로 구현)
|
||||
MeetingAnalysis analysis = performAIAnalysis(meeting, minutes);
|
||||
meetingAnalysisWriter.save(analysis);
|
||||
|
||||
// 6. 결과 DTO 구성
|
||||
MeetingEndDTO result = buildMeetingEndDTO(meeting, analysis);
|
||||
|
||||
log.info("Meeting ended successfully: {}", meetingId);
|
||||
return updatedMeeting;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 수행 (Mock 구현)
|
||||
*/
|
||||
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()
|
||||
);
|
||||
|
||||
return MeetingAnalysis.builder()
|
||||
.analysisId(UUID.randomUUID().toString())
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.keywords(keywords)
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status("COMPLETED")
|
||||
.completedAt(LocalDateTime.now())
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* MeetingEndDTO 구성
|
||||
*/
|
||||
private MeetingEndDTO buildMeetingEndDTO(Meeting meeting, MeetingAnalysis analysis) {
|
||||
// 회의 시간 계산 (Mock)
|
||||
int durationMinutes = 90;
|
||||
|
||||
// 전체 Todo 개수 계산
|
||||
int totalTodos = analysis.getAgendaAnalyses().stream()
|
||||
.mapToInt(agenda -> agenda.getExtractedTodos().size())
|
||||
.sum();
|
||||
|
||||
// 안건별 요약 DTO 변환
|
||||
List<MeetingEndDTO.AgendaSummaryDTO> agendaSummaries = analysis.getAgendaAnalyses().stream()
|
||||
.map(agenda -> MeetingEndDTO.AgendaSummaryDTO.builder()
|
||||
.title(agenda.getTitle())
|
||||
.aiSummaryShort(agenda.getAiSummaryShort())
|
||||
.details(MeetingEndDTO.AgendaDetailsDTO.builder()
|
||||
.discussion(agenda.getDiscussion())
|
||||
.decisions(agenda.getDecisions())
|
||||
.pending(agenda.getPending())
|
||||
.build())
|
||||
.todos(agenda.getExtractedTodos().stream()
|
||||
.map(todo -> MeetingEndDTO.TodoSummaryDTO.builder()
|
||||
.title(todo)
|
||||
.build())
|
||||
.toList())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
return MeetingEndDTO.builder()
|
||||
.title(meeting.getTitle())
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.durationMinutes(durationMinutes)
|
||||
.agendaCount(analysis.getAgendaAnalyses().size())
|
||||
.todoCount(totalTodos)
|
||||
.keywords(analysis.getKeywords())
|
||||
.agendaSummaries(agendaSummaries)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
|
||||
/**
|
||||
* 회의 종료 UseCase
|
||||
@ -8,7 +8,7 @@ import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
public interface EndMeetingUseCase {
|
||||
|
||||
/**
|
||||
* 회의 종료
|
||||
* 회의 종료 및 AI 분석
|
||||
*/
|
||||
Meeting endMeeting(String meetingId);
|
||||
MeetingEndDTO endMeeting(String meetingId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의 분석 Reader
|
||||
*/
|
||||
public interface MeetingAnalysisReader {
|
||||
|
||||
/**
|
||||
* 회의 ID로 분석 결과 조회
|
||||
*/
|
||||
Optional<MeetingAnalysis> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 분석 ID로 분석 결과 조회
|
||||
*/
|
||||
Optional<MeetingAnalysis> findById(String analysisId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 분석 결과 조회
|
||||
*/
|
||||
Optional<MeetingAnalysis> findByMinutesId(String minutesId);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
|
||||
/**
|
||||
* 회의 분석 Writer
|
||||
*/
|
||||
public interface MeetingAnalysisWriter {
|
||||
|
||||
/**
|
||||
* 분석 결과 저장
|
||||
*/
|
||||
MeetingAnalysis save(MeetingAnalysis analysis);
|
||||
|
||||
/**
|
||||
* 분석 결과 삭제
|
||||
*/
|
||||
void delete(String analysisId);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import com.unicorn.hgzero.meeting.infra.dto.request.InviteParticipantRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.SelectTemplateRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.InviteParticipantResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingEndResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@ -20,6 +21,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 관리 REST API Controller
|
||||
@ -169,15 +171,15 @@ public class MeetingController {
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param userId 사용자 ID
|
||||
* @return 회의 정보
|
||||
* @return 회의 종료 결과
|
||||
*/
|
||||
@Operation(
|
||||
summary = "회의 종료",
|
||||
description = "진행 중인 회의를 종료하고 회의록 작성을 완료합니다. 자동 Todo 추출 및 알림 발송이 수행됩니다.",
|
||||
description = "진행 중인 회의를 종료하고 AI 분석을 통해 회의록을 생성합니다. 주요 키워드 추출, 안건별 요약, Todo 자동 추출이 수행됩니다.",
|
||||
security = @SecurityRequirement(name = "bearerAuth")
|
||||
)
|
||||
@PostMapping("/{meetingId}/end")
|
||||
public ResponseEntity<ApiResponse<MeetingResponse>> endMeeting(
|
||||
public ResponseEntity<ApiResponse<MeetingEndResponse>> endMeeting(
|
||||
@Parameter(description = "회의 ID", required = true)
|
||||
@PathVariable String meetingId,
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@ -189,15 +191,102 @@ public class MeetingController {
|
||||
|
||||
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
var meetingData = endMeetingUseCase.endMeeting(meetingId);
|
||||
var meetingDTO = MeetingDTO.from(meetingData);
|
||||
var response = MeetingResponse.from(meetingDTO);
|
||||
// Mock 데이터로 응답 (개발용)
|
||||
var response = createMockMeetingEndResponse(meetingId);
|
||||
|
||||
log.info("회의 종료 완료 - meetingId: {}", meetingId);
|
||||
log.info("회의 종료 완료 (Mock) - meetingId: {}", meetingId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료 응답 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 정보 조회
|
||||
*
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 종료 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@Schema(description = "회의 종료 응답")
|
||||
public class MeetingEndResponse {
|
||||
|
||||
@Schema(description = "회의 제목", example = "Q1 전략 회의")
|
||||
private final String title;
|
||||
|
||||
@Schema(description = "참석자 수", example = "4")
|
||||
private final int participantCount;
|
||||
|
||||
@Schema(description = "회의 시간 (분)", example = "90")
|
||||
private final int durationMinutes;
|
||||
|
||||
@Schema(description = "주요 안건 수", example = "3")
|
||||
private final int agendaCount;
|
||||
|
||||
@Schema(description = "Todo 생성 수", example = "5")
|
||||
private final int todoCount;
|
||||
|
||||
@Schema(description = "주요 키워드")
|
||||
private final List<String> keywords;
|
||||
|
||||
@Schema(description = "안건별 AI 요약")
|
||||
private final List<AgendaSummary> agendaSummaries;
|
||||
|
||||
/**
|
||||
* MeetingEndDTO로부터 MeetingEndResponse 생성
|
||||
*/
|
||||
public static MeetingEndResponse from(MeetingEndDTO dto) {
|
||||
return MeetingEndResponse.builder()
|
||||
.title(dto.getTitle())
|
||||
.participantCount(dto.getParticipantCount())
|
||||
.durationMinutes(dto.getDurationMinutes())
|
||||
.agendaCount(dto.getAgendaCount())
|
||||
.todoCount(dto.getTodoCount())
|
||||
.keywords(dto.getKeywords())
|
||||
.agendaSummaries(dto.getAgendaSummaries().stream()
|
||||
.map(AgendaSummary::from)
|
||||
.toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@Schema(description = "안건 요약 정보")
|
||||
public static class AgendaSummary {
|
||||
@Schema(description = "안건 제목", example = "1. 신제품 기획 방향성")
|
||||
private final String title;
|
||||
|
||||
@Schema(description = "AI 요약 (간략)", example = "타겟 고객을 20-30대로 설정, UI/UX 개선 집중")
|
||||
private final String aiSummaryShort;
|
||||
|
||||
@Schema(description = "상세 내용")
|
||||
private final AgendaDetails details;
|
||||
|
||||
@Schema(description = "Todo 목록")
|
||||
private final List<TodoSummary> todos;
|
||||
|
||||
public static AgendaSummary from(MeetingEndDTO.AgendaSummaryDTO dto) {
|
||||
return AgendaSummary.builder()
|
||||
.title(dto.getTitle())
|
||||
.aiSummaryShort(dto.getAiSummaryShort())
|
||||
.details(AgendaDetails.from(dto.getDetails()))
|
||||
.todos(dto.getTodos().stream()
|
||||
.map(TodoSummary::from)
|
||||
.toList())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@Schema(description = "안건 상세 내용")
|
||||
public static class AgendaDetails {
|
||||
@Schema(description = "논의 주제", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
|
||||
private final String discussion;
|
||||
|
||||
@Schema(description = "결정 사항")
|
||||
private final List<String> decisions;
|
||||
|
||||
@Schema(description = "보류 사항")
|
||||
private final List<String> pending;
|
||||
|
||||
public static AgendaDetails from(MeetingEndDTO.AgendaDetailsDTO dto) {
|
||||
return AgendaDetails.builder()
|
||||
.discussion(dto.getDiscussion())
|
||||
.decisions(dto.getDecisions())
|
||||
.pending(dto.getPending())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@Schema(description = "Todo 요약 정보")
|
||||
public static class TodoSummary {
|
||||
@Schema(description = "Todo 제목", example = "시장 조사 보고서 작성")
|
||||
private final String title;
|
||||
|
||||
public static TodoSummary from(MeetingEndDTO.TodoSummaryDTO dto) {
|
||||
return TodoSummary.builder()
|
||||
.title(dto.getTitle())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의 분석 Gateway
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MeetingAnalysisGateway implements MeetingAnalysisReader, MeetingAnalysisWriter {
|
||||
|
||||
private final MeetingAnalysisJpaRepository repository;
|
||||
|
||||
@Override
|
||||
public Optional<MeetingAnalysis> findByMeetingId(String meetingId) {
|
||||
log.debug("Finding meeting analysis by meetingId: {}", meetingId);
|
||||
return repository.findByMeetingId(meetingId)
|
||||
.map(MeetingAnalysisEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MeetingAnalysis> findById(String analysisId) {
|
||||
log.debug("Finding meeting analysis by analysisId: {}", analysisId);
|
||||
return repository.findById(analysisId)
|
||||
.map(MeetingAnalysisEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MeetingAnalysis> findByMinutesId(String minutesId) {
|
||||
log.debug("Finding meeting analysis by minutesId: {}", minutesId);
|
||||
return repository.findByMinutesId(minutesId)
|
||||
.map(MeetingAnalysisEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MeetingAnalysis save(MeetingAnalysis analysis) {
|
||||
log.debug("Saving meeting analysis: {}", analysis.getAnalysisId());
|
||||
MeetingAnalysisEntity entity = MeetingAnalysisEntity.fromDomain(analysis);
|
||||
MeetingAnalysisEntity savedEntity = repository.save(entity);
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String analysisId) {
|
||||
log.debug("Deleting meeting analysis: {}", analysisId);
|
||||
repository.deleteById(analysisId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 분석 결과 Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "meeting_analysis")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MeetingAnalysisEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "analysis_id")
|
||||
private String analysisId;
|
||||
|
||||
@Column(name = "meeting_id", nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "minutes_id", nullable = false)
|
||||
private String minutesId;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "meeting_keywords", joinColumns = @JoinColumn(name = "analysis_id"))
|
||||
@Column(name = "keyword")
|
||||
private List<String> keywords;
|
||||
|
||||
@Column(name = "agenda_analyses", columnDefinition = "TEXT")
|
||||
private String agendaAnalysesJson; // JSON 문자열로 저장
|
||||
|
||||
@Column(name = "status")
|
||||
private String status;
|
||||
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Entity를 도메인으로 변환
|
||||
*/
|
||||
public MeetingAnalysis toDomain() {
|
||||
// JSON 파싱은 실제 구현에서는 ObjectMapper 사용
|
||||
// 현재는 Mock으로 처리
|
||||
return MeetingAnalysis.builder()
|
||||
.analysisId(this.analysisId)
|
||||
.meetingId(this.meetingId)
|
||||
.minutesId(this.minutesId)
|
||||
.keywords(this.keywords)
|
||||
.agendaAnalyses(List.of()) // Mock
|
||||
.status(this.status)
|
||||
.completedAt(this.completedAt)
|
||||
.createdAt(this.createdAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인에서 Entity로 변환
|
||||
*/
|
||||
public static MeetingAnalysisEntity fromDomain(MeetingAnalysis domain) {
|
||||
return MeetingAnalysisEntity.builder()
|
||||
.analysisId(domain.getAnalysisId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.minutesId(domain.getMinutesId())
|
||||
.keywords(domain.getKeywords())
|
||||
.agendaAnalysesJson("{}") // Mock JSON
|
||||
.status(domain.getStatus())
|
||||
.completedAt(domain.getCompletedAt())
|
||||
.createdAt(domain.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 회의 분석 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface MeetingAnalysisJpaRepository extends JpaRepository<MeetingAnalysisEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 분석 결과 조회
|
||||
*/
|
||||
Optional<MeetingAnalysisEntity> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 분석 결과 조회
|
||||
*/
|
||||
Optional<MeetingAnalysisEntity> findByMinutesId(String minutesId);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user