mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 20:46:23 +00:00
Merge pull request #11 from hwanny1128/feat/meeting
Feat: Meeting 관련 API 개발
This commit is contained in:
commit
da2421e805
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -119,7 +119,20 @@ public class Meeting {
|
|||||||
public void addParticipant(String participantEmail) {
|
public void addParticipant(String participantEmail) {
|
||||||
if (this.participants == null) {
|
if (this.participants == null) {
|
||||||
this.participants = new ArrayList<>();
|
this.participants = new ArrayList<>();
|
||||||
|
} else if (!(this.participants instanceof ArrayList)) {
|
||||||
|
// 불변 리스트인 경우 새로운 ArrayList로 변환
|
||||||
|
this.participants = new ArrayList<>(this.participants);
|
||||||
}
|
}
|
||||||
this.participants.add(participantEmail);
|
|
||||||
|
if (!this.participants.contains(participantEmail)) {
|
||||||
|
this.participants.add(participantEmail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 템플릿 적용
|
||||||
|
*/
|
||||||
|
public void applyTemplate(String templateId) {
|
||||||
|
this.templateId = templateId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.biz.service;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.ApplyTemplateUseCase;
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
||||||
|
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의록 템플릿 적용 서비스
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@Transactional
|
||||||
|
public class ApplyTemplateService implements ApplyTemplateUseCase {
|
||||||
|
|
||||||
|
private final MeetingReader meetingReader;
|
||||||
|
private final MeetingWriter meetingWriter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Meeting applyTemplate(ApplyTemplateCommand command) {
|
||||||
|
log.debug("템플릿 적용 시작 - meetingId: {}, templateId: {}",
|
||||||
|
command.meetingId(), command.templateId());
|
||||||
|
|
||||||
|
// 회의 조회
|
||||||
|
Meeting meeting = meetingReader.findById(command.meetingId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + command.meetingId()));
|
||||||
|
|
||||||
|
// 템플릿 적용 (회의 도메인에서 처리)
|
||||||
|
meeting.applyTemplate(command.templateId());
|
||||||
|
|
||||||
|
// 회의 정보 업데이트
|
||||||
|
Meeting updatedMeeting = meetingWriter.save(meeting);
|
||||||
|
|
||||||
|
log.debug("템플릿 적용 완료 - meetingId: {}, templateId: {}",
|
||||||
|
command.meetingId(), command.templateId());
|
||||||
|
|
||||||
|
return updatedMeeting;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,16 @@ package com.unicorn.hgzero.meeting.biz.service;
|
|||||||
import com.unicorn.hgzero.common.exception.BusinessException;
|
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||||
import com.unicorn.hgzero.common.exception.ErrorCode;
|
import com.unicorn.hgzero.common.exception.ErrorCode;
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
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.Minutes;
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.Session;
|
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.in.meeting.*;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
|
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.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.MinutesWriter;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
|
||||||
@ -42,7 +47,10 @@ public class MeetingService implements
|
|||||||
private final MeetingWriter meetingWriter;
|
private final MeetingWriter meetingWriter;
|
||||||
private final SessionReader sessionReader;
|
private final SessionReader sessionReader;
|
||||||
private final SessionWriter sessionWriter;
|
private final SessionWriter sessionWriter;
|
||||||
|
private final MinutesReader minutesReader;
|
||||||
private final MinutesWriter minutesWriter;
|
private final MinutesWriter minutesWriter;
|
||||||
|
private final MeetingAnalysisReader meetingAnalysisReader;
|
||||||
|
private final MeetingAnalysisWriter meetingAnalysisWriter;
|
||||||
private final CacheService cacheService;
|
private final CacheService cacheService;
|
||||||
private final EventPublisher eventPublisher;
|
private final EventPublisher eventPublisher;
|
||||||
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter participantWriter;
|
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter participantWriter;
|
||||||
@ -263,30 +271,165 @@ public class MeetingService implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 종료
|
* 회의 종료 및 AI 분석
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Meeting endMeeting(String meetingId) {
|
public MeetingEndDTO endMeeting(String meetingId) {
|
||||||
log.info("Ending meeting: {}", meetingId);
|
log.info("Ending meeting: {}", meetingId);
|
||||||
|
|
||||||
// 회의 조회
|
// 1. 회의 조회
|
||||||
|
log.debug("Searching for meeting with ID: {}", meetingId);
|
||||||
Meeting meeting = meetingReader.findById(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());
|
||||||
|
|
||||||
// 회의 상태 검증
|
// 2. 회의 상태 검증 (SCHEDULED 또는 IN_PROGRESS만 종료 가능)
|
||||||
if (!"IN_PROGRESS".equals(meeting.getStatus())) {
|
if (!"SCHEDULED".equals(meeting.getStatus()) && !"IN_PROGRESS".equals(meeting.getStatus())) {
|
||||||
throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
|
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.end();
|
||||||
|
|
||||||
// 저장
|
|
||||||
Meeting updatedMeeting = meetingWriter.save(meeting);
|
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);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
||||||
|
|
||||||
|
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의록 템플릿 적용 UseCase
|
||||||
|
*/
|
||||||
|
public interface ApplyTemplateUseCase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의에 템플릿을 적용
|
||||||
|
*
|
||||||
|
* @param command 템플릿 적용 명령
|
||||||
|
* @return 업데이트된 회의 정보
|
||||||
|
*/
|
||||||
|
Meeting applyTemplate(ApplyTemplateCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 템플릿 적용 명령
|
||||||
|
*/
|
||||||
|
record ApplyTemplateCommand(
|
||||||
|
String meetingId,
|
||||||
|
String templateId
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
|
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
|
* 회의 종료 UseCase
|
||||||
@ -8,7 +8,7 @@ import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
|||||||
public interface EndMeetingUseCase {
|
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);
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
package com.unicorn.hgzero.meeting.infra.controller;
|
package com.unicorn.hgzero.meeting.infra.controller;
|
||||||
|
|
||||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||||
import com.unicorn.hgzero.meeting.biz.dto.DashboardDTO;
|
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.in.dashboard.GetDashboardUseCase;
|
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.response.DashboardResponse;
|
import com.unicorn.hgzero.meeting.infra.dto.response.DashboardResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@ -14,6 +12,11 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 대시보드 REST API Controller
|
* 대시보드 REST API Controller
|
||||||
* 사용자별 맞춤 대시보드 데이터 제공
|
* 사용자별 맞춤 대시보드 데이터 제공
|
||||||
@ -25,17 +28,15 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class DashboardController {
|
public class DashboardController {
|
||||||
|
|
||||||
private final GetDashboardUseCase getDashboardUseCase;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 대시보드 데이터 조회
|
* 대시보드 데이터 조회 (목 데이터)
|
||||||
*
|
*
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return 대시보드 데이터
|
* @return 대시보드 데이터
|
||||||
*/
|
*/
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "대시보드 데이터 조회",
|
summary = "대시보드 데이터 조회",
|
||||||
description = "사용자별 맞춤 대시보드 정보를 조회합니다. 예정된 회의 목록, 진행 중 Todo 목록, 최근 회의록 목록, 통계 정보를 포함합니다.",
|
description = "사용자별 맞춤 대시보드 정보를 조회합니다. 예정된 회의 목록, 최근 회의록 목록, 통계 정보를 포함합니다.",
|
||||||
security = @SecurityRequirement(name = "bearerAuth")
|
security = @SecurityRequirement(name = "bearerAuth")
|
||||||
)
|
)
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -49,12 +50,80 @@ public class DashboardController {
|
|||||||
|
|
||||||
log.info("대시보드 데이터 조회 요청 - userId: {}", userId);
|
log.info("대시보드 데이터 조회 요청 - userId: {}", userId);
|
||||||
|
|
||||||
var dashboardData = getDashboardUseCase.getDashboard(userId);
|
// 목 데이터 생성
|
||||||
var dashboardDTO = DashboardDTO.from(dashboardData);
|
DashboardResponse mockResponse = createMockDashboardData();
|
||||||
var response = DashboardResponse.from(dashboardDTO);
|
|
||||||
|
|
||||||
log.info("대시보드 데이터 조회 완료 - userId: {}", userId);
|
log.info("대시보드 데이터 조회 완료 - userId: {}", userId);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(mockResponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 목 데이터 생성
|
||||||
|
*/
|
||||||
|
private DashboardResponse createMockDashboardData() {
|
||||||
|
// 예정된 회의 목 데이터
|
||||||
|
List<DashboardResponse.UpcomingMeetingResponse> upcomingMeetings = Arrays.asList(
|
||||||
|
DashboardResponse.UpcomingMeetingResponse.builder()
|
||||||
|
.meetingId("550e8400-e29b-41d4-a716-446655440001")
|
||||||
|
.title("Q1 전략 회의")
|
||||||
|
.startTime(LocalDateTime.now().plusDays(2).withHour(14).withMinute(0))
|
||||||
|
.endTime(LocalDateTime.now().plusDays(2).withHour(16).withMinute(0))
|
||||||
|
.location("회의실 A")
|
||||||
|
.participantCount(5)
|
||||||
|
.status("SCHEDULED")
|
||||||
|
.build(),
|
||||||
|
DashboardResponse.UpcomingMeetingResponse.builder()
|
||||||
|
.meetingId("550e8400-e29b-41d4-a716-446655440002")
|
||||||
|
.title("개발팀 스프린트 계획")
|
||||||
|
.startTime(LocalDateTime.now().plusDays(3).withHour(10).withMinute(0))
|
||||||
|
.endTime(LocalDateTime.now().plusDays(3).withHour(12).withMinute(0))
|
||||||
|
.location("회의실 B")
|
||||||
|
.participantCount(8)
|
||||||
|
.status("SCHEDULED")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 최근 회의록 목 데이터
|
||||||
|
List<DashboardResponse.RecentMinutesResponse> recentMinutes = Arrays.asList(
|
||||||
|
DashboardResponse.RecentMinutesResponse.builder()
|
||||||
|
.minutesId("770e8400-e29b-41d4-a716-446655440001")
|
||||||
|
.title("아키텍처 설계 회의")
|
||||||
|
.meetingDate(LocalDateTime.now().minusDays(1).withHour(14).withMinute(0))
|
||||||
|
.status("FINALIZED")
|
||||||
|
.participantCount(6)
|
||||||
|
.lastModified(LocalDateTime.now().minusDays(1).withHour(16).withMinute(30))
|
||||||
|
.build(),
|
||||||
|
DashboardResponse.RecentMinutesResponse.builder()
|
||||||
|
.minutesId("770e8400-e29b-41d4-a716-446655440002")
|
||||||
|
.title("UI/UX 검토 회의")
|
||||||
|
.meetingDate(LocalDateTime.now().minusDays(3).withHour(11).withMinute(0))
|
||||||
|
.status("FINALIZED")
|
||||||
|
.participantCount(4)
|
||||||
|
.lastModified(LocalDateTime.now().minusDays(3).withHour(12).withMinute(45))
|
||||||
|
.build(),
|
||||||
|
DashboardResponse.RecentMinutesResponse.builder()
|
||||||
|
.minutesId("770e8400-e29b-41d4-a716-446655440003")
|
||||||
|
.title("API 설계 검토")
|
||||||
|
.meetingDate(LocalDateTime.now().minusDays(5).withHour(15).withMinute(0))
|
||||||
|
.status("DRAFT")
|
||||||
|
.participantCount(3)
|
||||||
|
.lastModified(LocalDateTime.now().minusDays(5).withHour(16).withMinute(15))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 통계 정보 목 데이터
|
||||||
|
DashboardResponse.StatisticsResponse statistics = DashboardResponse.StatisticsResponse.builder()
|
||||||
|
.upcomingMeetingsCount(2)
|
||||||
|
.activeTodosCount(0) // activeTodos 제거로 0으로 설정
|
||||||
|
.todoCompletionRate(0.0) // activeTodos 제거로 0으로 설정
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return DashboardResponse.builder()
|
||||||
|
.upcomingMeetings(upcomingMeetings)
|
||||||
|
.activeTodos(Collections.emptyList()) // activeTodos 빈 리스트로 설정
|
||||||
|
.myMinutes(recentMinutes)
|
||||||
|
.statistics(statistics)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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.request.SelectTemplateRequest;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.response.InviteParticipantResponse;
|
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.MeetingResponse;
|
||||||
|
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingEndResponse;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
|
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@ -20,6 +21,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 관리 REST API Controller
|
* 회의 관리 REST API Controller
|
||||||
@ -38,6 +40,7 @@ public class MeetingController {
|
|||||||
private final GetMeetingUseCase getMeetingUseCase;
|
private final GetMeetingUseCase getMeetingUseCase;
|
||||||
private final CancelMeetingUseCase cancelMeetingUseCase;
|
private final CancelMeetingUseCase cancelMeetingUseCase;
|
||||||
private final InviteParticipantUseCase inviteParticipantUseCase;
|
private final InviteParticipantUseCase inviteParticipantUseCase;
|
||||||
|
private final ApplyTemplateUseCase applyTemplateUseCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 예약
|
* 회의 예약
|
||||||
@ -113,11 +116,17 @@ public class MeetingController {
|
|||||||
|
|
||||||
log.info("템플릿 적용 요청 - meetingId: {}, templateId: {}", meetingId, request.getTemplateId());
|
log.info("템플릿 적용 요청 - meetingId: {}, templateId: {}", meetingId, request.getTemplateId());
|
||||||
|
|
||||||
var meetingData = getMeetingUseCase.getMeeting(meetingId);
|
var meetingData = applyTemplateUseCase.applyTemplate(
|
||||||
|
new ApplyTemplateUseCase.ApplyTemplateCommand(
|
||||||
|
meetingId,
|
||||||
|
request.getTemplateId()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
var meetingDTO = MeetingDTO.from(meetingData);
|
var meetingDTO = MeetingDTO.from(meetingData);
|
||||||
var response = MeetingResponse.from(meetingDTO);
|
var response = MeetingResponse.from(meetingDTO);
|
||||||
|
|
||||||
log.info("템플릿 적용 완료 - meetingId: {}", meetingId);
|
log.info("템플릿 적용 완료 - meetingId: {}, templateId: {}", meetingId, request.getTemplateId());
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
}
|
}
|
||||||
@ -162,15 +171,15 @@ public class MeetingController {
|
|||||||
*
|
*
|
||||||
* @param meetingId 회의 ID
|
* @param meetingId 회의 ID
|
||||||
* @param userId 사용자 ID
|
* @param userId 사용자 ID
|
||||||
* @return 회의 정보
|
* @return 회의 종료 결과
|
||||||
*/
|
*/
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "회의 종료",
|
summary = "회의 종료",
|
||||||
description = "진행 중인 회의를 종료하고 회의록 작성을 완료합니다. 자동 Todo 추출 및 알림 발송이 수행됩니다.",
|
description = "진행 중인 회의를 종료하고 AI 분석을 통해 회의록을 생성합니다. 주요 키워드 추출, 안건별 요약, Todo 자동 추출이 수행됩니다.",
|
||||||
security = @SecurityRequirement(name = "bearerAuth")
|
security = @SecurityRequirement(name = "bearerAuth")
|
||||||
)
|
)
|
||||||
@PostMapping("/{meetingId}/end")
|
@PostMapping("/{meetingId}/end")
|
||||||
public ResponseEntity<ApiResponse<MeetingResponse>> endMeeting(
|
public ResponseEntity<ApiResponse<MeetingEndResponse>> endMeeting(
|
||||||
@Parameter(description = "회의 ID", required = true)
|
@Parameter(description = "회의 ID", required = true)
|
||||||
@PathVariable String meetingId,
|
@PathVariable String meetingId,
|
||||||
@Parameter(description = "사용자 ID", required = true)
|
@Parameter(description = "사용자 ID", required = true)
|
||||||
@ -182,15 +191,102 @@ public class MeetingController {
|
|||||||
|
|
||||||
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||||
|
|
||||||
var meetingData = endMeetingUseCase.endMeeting(meetingId);
|
// Mock 데이터로 응답 (개발용)
|
||||||
var meetingDTO = MeetingDTO.from(meetingData);
|
var response = createMockMeetingEndResponse(meetingId);
|
||||||
var response = MeetingResponse.from(meetingDTO);
|
|
||||||
|
|
||||||
log.info("회의 종료 완료 - meetingId: {}", meetingId);
|
log.info("회의 종료 완료 (Mock) - meetingId: {}", meetingId);
|
||||||
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 정보 조회
|
* 회의 정보 조회
|
||||||
*
|
*
|
||||||
|
|||||||
@ -1,13 +1,8 @@
|
|||||||
package com.unicorn.hgzero.meeting.infra.controller;
|
package com.unicorn.hgzero.meeting.infra.controller;
|
||||||
|
|
||||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||||
import com.unicorn.hgzero.meeting.biz.dto.TemplateDTO;
|
|
||||||
import com.unicorn.hgzero.meeting.biz.service.TemplateService;
|
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.response.TemplateListResponse;
|
import com.unicorn.hgzero.meeting.infra.dto.response.TemplateListResponse;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.response.TemplateDetailResponse;
|
|
||||||
import com.unicorn.hgzero.meeting.infra.cache.CacheService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -15,12 +10,14 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 템플릿 관리 API Controller
|
* 템플릿 관리API Controller
|
||||||
* 템플릿 목록 조회, 상세 조회 기능
|
* 고정된 템플릿 목록을 제공합니다
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/templates")
|
@RequestMapping("/api/templates")
|
||||||
@ -29,11 +26,8 @@ import java.util.stream.Collectors;
|
|||||||
@Tag(name = "Template", description = "템플릿 관리 API")
|
@Tag(name = "Template", description = "템플릿 관리 API")
|
||||||
public class TemplateController {
|
public class TemplateController {
|
||||||
|
|
||||||
private final TemplateService templateService;
|
|
||||||
private final CacheService cacheService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 템플릿 목록 조회
|
* 템플릿 목록 조회 (고정 데이터 반환)
|
||||||
* GET /api/templates
|
* GET /api/templates
|
||||||
*/
|
*/
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@ -45,40 +39,19 @@ public class TemplateController {
|
|||||||
})
|
})
|
||||||
public ResponseEntity<ApiResponse<TemplateListResponse>> getTemplateList(
|
public ResponseEntity<ApiResponse<TemplateListResponse>> getTemplateList(
|
||||||
@RequestHeader("X-User-Id") String userId,
|
@RequestHeader("X-User-Id") String userId,
|
||||||
@RequestHeader("X-User-Name") String userName,
|
@RequestHeader("X-User-Name") String userName) {
|
||||||
@Parameter(description = "템플릿 카테고리") @RequestParam(required = false) String category,
|
|
||||||
@Parameter(description = "활성 상태 (true: 활성, false: 비활성)") @RequestParam(required = false) Boolean isActive) {
|
|
||||||
|
|
||||||
log.info("템플릿 목록 조회 요청 - userId: {}, category: {}, isActive: {}",
|
log.info("템플릿 목록 조회 요청 - userId: {}", userId);
|
||||||
userId, category, isActive);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 캐시 확인
|
// 고정된 템플릿 데이터 생성
|
||||||
String cacheKey = String.format("templates:list:%s:%s",
|
List<TemplateListResponse.TemplateItem> templateItems = createFixedTemplates();
|
||||||
(category != null ? category : "all"),
|
|
||||||
(isActive != null ? isActive.toString() : "all"));
|
|
||||||
TemplateListResponse cachedResponse = cacheService.getCachedTemplateList(cacheKey);
|
|
||||||
if (cachedResponse != null) {
|
|
||||||
log.debug("캐시된 템플릿 목록 반환");
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(cachedResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 템플릿 목록 조회
|
|
||||||
List<TemplateDTO> templates = templateService.getTemplateList(category, isActive);
|
|
||||||
|
|
||||||
// 응답 DTO 생성
|
|
||||||
List<TemplateListResponse.TemplateItem> templateItems = templates.stream()
|
|
||||||
.map(this::convertToTemplateItem)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
TemplateListResponse response = TemplateListResponse.builder()
|
TemplateListResponse response = TemplateListResponse.builder()
|
||||||
.templateList(templateItems)
|
.templateList(templateItems)
|
||||||
.totalCount(templateItems.size())
|
.totalCount(templateItems.size())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 캐시 저장
|
|
||||||
cacheService.cacheTemplateList(cacheKey, response);
|
|
||||||
|
|
||||||
log.info("템플릿 목록 조회 성공 - count: {}", templateItems.size());
|
log.info("템플릿 목록 조회 성공 - count: {}", templateItems.size());
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
|
||||||
@ -90,99 +63,103 @@ public class TemplateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 템플릿 상세 조회
|
* 고정된 템플릿 데이터 생성
|
||||||
* GET /api/templates/{templateId}
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{templateId}")
|
private List<TemplateListResponse.TemplateItem> createFixedTemplates() {
|
||||||
@Operation(summary = "템플릿 상세 조회", description = "템플릿 상세 정보를 조회합니다")
|
List<TemplateListResponse.TemplateItem> templates = new ArrayList<>();
|
||||||
public ResponseEntity<ApiResponse<TemplateDetailResponse>> getTemplateDetail(
|
|
||||||
@RequestHeader("X-User-Id") String userId,
|
|
||||||
@RequestHeader("X-User-Name") String userName,
|
|
||||||
@Parameter(description = "템플릿 ID") @PathVariable String templateId) {
|
|
||||||
|
|
||||||
log.info("템플릿 상세 조회 요청 - userId: {}, templateId: {}", userId, templateId);
|
// 일반 회의 템플릿
|
||||||
|
templates.add(TemplateListResponse.TemplateItem.builder()
|
||||||
|
.templateId("general")
|
||||||
|
.name("일반 회의")
|
||||||
|
.description("기본 회의록 형식")
|
||||||
|
.category("meeting")
|
||||||
|
.icon("📋")
|
||||||
|
.isActive(true)
|
||||||
|
.usageCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.lastUsedAt(null)
|
||||||
|
.createdBy("system")
|
||||||
|
.sections(Arrays.asList(
|
||||||
|
createSectionInfo("회의 개요", "회의 기본 정보", 1, true),
|
||||||
|
createSectionInfo("논의 사항", "주요 논의 내용", 2, true),
|
||||||
|
createSectionInfo("결정 사항", "회의에서 결정된 사항", 3, true),
|
||||||
|
createSectionInfo("액션 아이템", "향후 진행할 작업", 4, true)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
try {
|
// 스크럼 회의 템플릿
|
||||||
// 캐시 확인
|
templates.add(TemplateListResponse.TemplateItem.builder()
|
||||||
TemplateDetailResponse cachedResponse = cacheService.getCachedTemplateDetail(templateId);
|
.templateId("scrum")
|
||||||
if (cachedResponse != null) {
|
.name("스크럼 회의")
|
||||||
log.debug("캐시된 템플릿 상세 반환 - templateId: {}", templateId);
|
.description("데일리 스탠드업 형식")
|
||||||
return ResponseEntity.ok(ApiResponse.success(cachedResponse));
|
.category("agile")
|
||||||
}
|
.icon("🏃")
|
||||||
|
.isActive(true)
|
||||||
|
.usageCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.lastUsedAt(null)
|
||||||
|
.createdBy("system")
|
||||||
|
.sections(Arrays.asList(
|
||||||
|
createSectionInfo("어제 한 일", "지난 작업일에 완료한 작업", 1, true),
|
||||||
|
createSectionInfo("오늘 할 일", "오늘 진행할 예정 작업", 2, true),
|
||||||
|
createSectionInfo("블로커/이슈", "진행을 방해하는 요소", 3, false)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
// 템플릿 조회
|
// 킥오프 회의 템플릿
|
||||||
TemplateDTO templateDTO = templateService.getTemplateById(templateId);
|
templates.add(TemplateListResponse.TemplateItem.builder()
|
||||||
|
.templateId("kickoff")
|
||||||
|
.name("킥오프 회의")
|
||||||
|
.description("프로젝트 시작 회의")
|
||||||
|
.category("project")
|
||||||
|
.icon("🚀")
|
||||||
|
.isActive(true)
|
||||||
|
.usageCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.lastUsedAt(null)
|
||||||
|
.createdBy("system")
|
||||||
|
.sections(Arrays.asList(
|
||||||
|
createSectionInfo("프로젝트 개요", "프로젝트 기본 정보", 1, true),
|
||||||
|
createSectionInfo("목표 및 범위", "프로젝트 목표와 범위", 2, true),
|
||||||
|
createSectionInfo("역할 및 책임", "팀원별 역할과 책임", 3, true),
|
||||||
|
createSectionInfo("일정 및 마일스톤", "프로젝트 일정", 4, true)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
// 응답 DTO 생성
|
// 주간 회의 템플릿
|
||||||
TemplateDetailResponse response = convertToTemplateDetailResponse(templateDTO);
|
templates.add(TemplateListResponse.TemplateItem.builder()
|
||||||
|
.templateId("weekly")
|
||||||
|
.name("주간 회의")
|
||||||
|
.description("주간 리뷰 및 계획")
|
||||||
|
.category("review")
|
||||||
|
.icon("📅")
|
||||||
|
.isActive(true)
|
||||||
|
.usageCount(0)
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.lastUsedAt(null)
|
||||||
|
.createdBy("system")
|
||||||
|
.sections(Arrays.asList(
|
||||||
|
createSectionInfo("지난주 성과", "지난주 달성한 성과", 1, true),
|
||||||
|
createSectionInfo("이번주 계획", "이번주 진행할 계획", 2, true),
|
||||||
|
createSectionInfo("주요 이슈", "해결이 필요한 이슈", 3, false),
|
||||||
|
createSectionInfo("다음 액션", "다음 주 액션 아이템", 4, true)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
|
||||||
// 캐시 저장
|
return templates;
|
||||||
cacheService.cacheTemplateDetail(templateId, response);
|
|
||||||
|
|
||||||
log.info("템플릿 상세 조회 성공 - templateId: {}", templateId);
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(response));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("템플릿 상세 조회 실패 - templateId: {}", templateId, e);
|
|
||||||
return ResponseEntity.badRequest()
|
|
||||||
.body(ApiResponse.errorWithType("템플릿 상세 조회에 실패했습니다"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
/**
|
||||||
private TemplateListResponse.TemplateItem convertToTemplateItem(TemplateDTO templateDTO) {
|
* 템플릿 섹션 정보 생성 헬퍼 메서드
|
||||||
// 섹션 정보 변환
|
*/
|
||||||
List<TemplateListResponse.TemplateSectionInfo> sections = templateDTO.getSections().stream()
|
private TemplateListResponse.TemplateSectionInfo createSectionInfo(
|
||||||
.map(section -> TemplateListResponse.TemplateSectionInfo.builder()
|
String title, String description, int orderIndex, boolean isRequired) {
|
||||||
.title(section.getTitle())
|
return TemplateListResponse.TemplateSectionInfo.builder()
|
||||||
.description(section.getDescription())
|
.title(title)
|
||||||
.orderIndex(section.getOrderIndex())
|
.description(description)
|
||||||
.isRequired(section.isRequired())
|
.orderIndex(orderIndex)
|
||||||
.build())
|
.isRequired(isRequired)
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return TemplateListResponse.TemplateItem.builder()
|
|
||||||
.templateId(templateDTO.getTemplateId())
|
|
||||||
.name(templateDTO.getName())
|
|
||||||
.description(templateDTO.getDescription())
|
|
||||||
.category(templateDTO.getCategory())
|
|
||||||
.isActive(templateDTO.isActive())
|
|
||||||
.usageCount(templateDTO.getUsageCount())
|
|
||||||
.createdAt(templateDTO.getCreatedAt())
|
|
||||||
.lastUsedAt(templateDTO.getLastUsedAt())
|
|
||||||
.createdBy(templateDTO.getCreatedBy())
|
|
||||||
.sections(sections)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private TemplateDetailResponse convertToTemplateDetailResponse(TemplateDTO templateDTO) {
|
|
||||||
// 섹션 상세 정보 변환
|
|
||||||
List<TemplateDetailResponse.SectionDetail> sections = templateDTO.getSections().stream()
|
|
||||||
.map(section -> TemplateDetailResponse.SectionDetail.builder()
|
|
||||||
.sectionId(section.getSectionId())
|
|
||||||
.title(section.getTitle())
|
|
||||||
.description(section.getDescription())
|
|
||||||
.content(section.getContent())
|
|
||||||
.orderIndex(section.getOrderIndex())
|
|
||||||
.isRequired(section.isRequired())
|
|
||||||
.inputType(section.getInputType())
|
|
||||||
.placeholder(section.getPlaceholder())
|
|
||||||
.maxLength(section.getMaxLength())
|
|
||||||
.isEditable(section.isEditable())
|
|
||||||
.build())
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return TemplateDetailResponse.builder()
|
|
||||||
.templateId(templateDTO.getTemplateId())
|
|
||||||
.name(templateDTO.getName())
|
|
||||||
.description(templateDTO.getDescription())
|
|
||||||
.category(templateDTO.getCategory())
|
|
||||||
.isActive(templateDTO.isActive())
|
|
||||||
.usageCount(templateDTO.getUsageCount())
|
|
||||||
.createdAt(templateDTO.getCreatedAt())
|
|
||||||
.lastUsedAt(templateDTO.getLastUsedAt())
|
|
||||||
.createdBy(templateDTO.getCreatedBy())
|
|
||||||
.sections(sections)
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,9 +17,7 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
public class SelectTemplateRequest {
|
public class SelectTemplateRequest {
|
||||||
|
|
||||||
@NotBlank(message = "템플릿 ID는 필수입니다")
|
@NotBlank(message = "템플릿 ID는 필수입니다")
|
||||||
@Schema(description = "템플릿 ID", example = "template-001", required = true)
|
@Schema(description = "템플릿 ID", example = "general", required = true,
|
||||||
|
allowableValues = {"general", "scrum", "kickoff", "weekly"})
|
||||||
private String templateId;
|
private String templateId;
|
||||||
|
|
||||||
@Schema(description = "커스터마이징 옵션", example = "섹션 순서 변경 또는 추가 섹션 포함")
|
|
||||||
private String customization;
|
|
||||||
}
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ public class TemplateListResponse {
|
|||||||
private String name;
|
private String name;
|
||||||
private String description;
|
private String description;
|
||||||
private String category;
|
private String category;
|
||||||
|
private String icon; // 아이콘 추가
|
||||||
private boolean isActive;
|
private boolean isActive;
|
||||||
private int usageCount;
|
private int usageCount;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|||||||
@ -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