mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 11:49:10 +00:00
작업 중: Meeting AI 통합 개발 진행 상황 저장
This commit is contained in:
+49
-20
@@ -4,18 +4,19 @@ import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.EndMeetingUseCase;
|
||||
import com.unicorn.hgzero.meeting.infra.client.AIServiceClient;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.AgendaSummaryDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ConsolidateResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ExtractedTodoDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.ai.ParticipantMinutesDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesSectionJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -25,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -39,7 +41,8 @@ import java.util.stream.Collectors;
|
||||
public class EndMeetingService implements EndMeetingUseCase {
|
||||
|
||||
private final MeetingJpaRepository meetingRepository;
|
||||
private final AgendaSectionJpaRepository agendaRepository;
|
||||
private final MinutesJpaRepository minutesRepository;
|
||||
private final MinutesSectionJpaRepository minutesSectionRepository;
|
||||
private final TodoJpaRepository todoRepository;
|
||||
private final MeetingAnalysisJpaRepository analysisRepository;
|
||||
private final AIServiceClient aiServiceClient;
|
||||
@@ -59,13 +62,26 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
MeetingEntity meeting = meetingRepository.findById(meetingId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + meetingId));
|
||||
|
||||
// 2. 안건 목록 조회 (실제로는 참석자별 메모 섹션)
|
||||
List<AgendaSectionEntity> agendaSections = agendaRepository.findByMeetingIdOrderByAgendaNumberAsc(meetingId);
|
||||
// 2. 참석자별 회의록 조회 (userId가 있는 회의록들)
|
||||
List<MinutesEntity> participantMinutesList = minutesRepository.findByMeetingIdAndUserIdIsNotNull(meetingId);
|
||||
|
||||
// 3. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, agendaSections);
|
||||
if (participantMinutesList.isEmpty()) {
|
||||
throw new IllegalStateException("참석자 회의록이 없습니다: " + meetingId);
|
||||
}
|
||||
|
||||
// 4. AI Service 호출
|
||||
// 3. 각 회의록의 sections 조회 및 통합
|
||||
List<MinutesSectionEntity> allMinutesSections = new ArrayList<>();
|
||||
for (MinutesEntity minutes : participantMinutesList) {
|
||||
List<MinutesSectionEntity> sections = minutesSectionRepository.findByMinutesIdOrderByOrderAsc(
|
||||
minutes.getMinutesId()
|
||||
);
|
||||
allMinutesSections.addAll(sections);
|
||||
}
|
||||
|
||||
// 4. AI 통합 분석 요청 데이터 생성
|
||||
ConsolidateRequest request = createConsolidateRequest(meeting, allMinutesSections, participantMinutesList);
|
||||
|
||||
// 5. AI Service 호출
|
||||
ConsolidateResponse aiResponse = aiServiceClient.consolidateMinutes(request);
|
||||
|
||||
// 5. AI 분석 결과 저장
|
||||
@@ -74,25 +90,38 @@ public class EndMeetingService implements EndMeetingUseCase {
|
||||
// 6. Todo 생성 및 저장
|
||||
List<TodoEntity> todos = createAndSaveTodos(meeting, aiResponse, analysis);
|
||||
|
||||
// 7. 회의 종료 처리
|
||||
// 6. 회의 종료 처리
|
||||
meeting.end();
|
||||
meetingRepository.save(meeting);
|
||||
|
||||
// 8. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, agendaSections.size());
|
||||
// 7. 응답 DTO 생성
|
||||
return createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 통합 분석 요청 데이터 생성
|
||||
* 참석자별 회의록의 섹션들을 참석자별로 그룹화하여 AI 요청 데이터 생성
|
||||
*/
|
||||
private ConsolidateRequest createConsolidateRequest(MeetingEntity meeting, List<AgendaSectionEntity> agendaSections) {
|
||||
// 참석자별 회의록 변환 (AgendaSection → ParticipantMinutes)
|
||||
List<ParticipantMinutesDTO> participantMinutes = agendaSections.stream()
|
||||
.<ParticipantMinutesDTO>map(section -> ParticipantMinutesDTO.builder()
|
||||
.userId(section.getMeetingId()) // 실제로는 participantId 필요
|
||||
.userName(section.getAgendaTitle()) // 실제로는 participantName 필요
|
||||
.content(section.getDiscussions() != null ? section.getDiscussions() : "")
|
||||
.build())
|
||||
private ConsolidateRequest createConsolidateRequest(
|
||||
MeetingEntity meeting,
|
||||
List<MinutesSectionEntity> allMinutesSections,
|
||||
List<MinutesEntity> participantMinutesList) {
|
||||
|
||||
// 참석자별 회의록을 ParticipantMinutesDTO로 변환
|
||||
List<ParticipantMinutesDTO> participantMinutes = participantMinutesList.stream()
|
||||
.<ParticipantMinutesDTO>map(minutes -> {
|
||||
// 해당 회의록의 섹션들만 필터링
|
||||
String content = allMinutesSections.stream()
|
||||
.filter(section -> section.getMinutesId().equals(minutes.getMinutesId()))
|
||||
.<String>map(section -> section.getTitle() + "\n" + section.getContent())
|
||||
.collect(Collectors.joining("\n\n"));
|
||||
|
||||
return ParticipantMinutesDTO.builder()
|
||||
.userId(minutes.getUserId())
|
||||
.userName(minutes.getUserId()) // 실제로는 userName이 필요하지만 일단 userId 사용
|
||||
.content(content)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ConsolidateRequest.builder()
|
||||
|
||||
@@ -46,7 +46,7 @@ public class AIServiceClient {
|
||||
log.info("AI Service 호출 - 회의록 통합 요약: {}", request.getMeetingId());
|
||||
|
||||
try {
|
||||
String url = aiServiceUrl + "/api/v1/transcripts/consolidate";
|
||||
String url = aiServiceUrl + "/api/transcripts/consolidate";
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
+1
-12
@@ -37,10 +37,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@OneToMany(mappedBy = "minutes", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<MinutesSectionEntity> sections = new ArrayList<>();
|
||||
|
||||
@Column(name = "status", length = 20, nullable = false)
|
||||
@Builder.Default
|
||||
private String status = "DRAFT";
|
||||
@@ -64,9 +60,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(this.meetingId)
|
||||
.userId(this.userId)
|
||||
.title(this.title)
|
||||
.sections(this.sections.stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
.collect(Collectors.toList()))
|
||||
.sections(List.of()) // sections는 별도 조회 필요
|
||||
.status(this.status)
|
||||
.version(this.version)
|
||||
.createdBy(this.createdBy)
|
||||
@@ -83,11 +77,6 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.meetingId(minutes.getMeetingId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.sections(minutes.getSections() != null
|
||||
? minutes.getSections().stream()
|
||||
.map(MinutesSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList())
|
||||
: new ArrayList<>())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
|
||||
+19
-24
@@ -10,6 +10,8 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 회의록 섹션 Entity
|
||||
* 참석자가 작성한 메모를 안건별로 저장
|
||||
* AI 분석의 입력 데이터로 사용됨
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "minutes_sections")
|
||||
@@ -20,43 +22,36 @@ import lombok.NoArgsConstructor;
|
||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "section_id", length = 50)
|
||||
private String sectionId;
|
||||
@Column(name = "id", length = 50)
|
||||
private String id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "minutes_id", nullable = false)
|
||||
private MinutesEntity minutes;
|
||||
|
||||
@Column(name = "minutes_id", insertable = false, updatable = false)
|
||||
@Column(name = "minutes_id", nullable = false, length = 50)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "type", length = 50, nullable = false)
|
||||
@Column(name = "type", length = 50)
|
||||
private String type;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
@Column(name = "title", length = 200)
|
||||
private String title;
|
||||
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "\"order\"")
|
||||
@Builder.Default
|
||||
private Integer order = 0;
|
||||
@Column(name = "order")
|
||||
private Integer order;
|
||||
|
||||
@Column(name = "verified", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean verified = false;
|
||||
@Column(name = "verified")
|
||||
private Boolean verified;
|
||||
|
||||
@Column(name = "locked", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean locked = false;
|
||||
@Column(name = "locked")
|
||||
private Boolean locked;
|
||||
|
||||
@Column(name = "locked_by", length = 50)
|
||||
private String lockedBy;
|
||||
|
||||
public MinutesSection toDomain() {
|
||||
return MinutesSection.builder()
|
||||
.sectionId(this.sectionId)
|
||||
.sectionId(this.id)
|
||||
.minutesId(this.minutesId)
|
||||
.type(this.type)
|
||||
.title(this.title)
|
||||
@@ -70,7 +65,7 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
public static MinutesSectionEntity fromDomain(MinutesSection section) {
|
||||
return MinutesSectionEntity.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.id(section.getSectionId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.type(section.getType())
|
||||
.title(section.getTitle())
|
||||
@@ -82,6 +77,10 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
.build();
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
|
||||
public void lock(String userId) {
|
||||
this.locked = true;
|
||||
this.lockedBy = userId;
|
||||
@@ -91,8 +90,4 @@ public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
this.locked = false;
|
||||
this.lockedBy = null;
|
||||
}
|
||||
|
||||
public void verify() {
|
||||
this.verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ spring:
|
||||
use_sql_comments: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
ddl-auto: ${JPA_DDL_AUTO:none}
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- ========================================
|
||||
-- V5: minutes_sections 테이블 재생성
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-28
|
||||
-- 설명: minutes_sections 테이블을 Entity 구조에 맞게 재생성
|
||||
|
||||
-- 1. 기존 테이블이 있으면 삭제
|
||||
DROP TABLE IF EXISTS minutes_sections CASCADE;
|
||||
|
||||
-- 2. Entity 구조에 맞는 테이블 생성
|
||||
CREATE TABLE minutes_sections (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
minutes_id VARCHAR(50) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
title VARCHAR(200),
|
||||
content TEXT,
|
||||
"order" INTEGER,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
locked BOOLEAN DEFAULT FALSE,
|
||||
locked_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_minutes_sections_minutes
|
||||
FOREIGN KEY (minutes_id) REFERENCES minutes(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. 인덱스 생성
|
||||
CREATE INDEX idx_minutes_sections_minutes ON minutes_sections(minutes_id);
|
||||
CREATE INDEX idx_minutes_sections_order ON minutes_sections(minutes_id, "order");
|
||||
CREATE INDEX idx_minutes_sections_type ON minutes_sections(type);
|
||||
CREATE INDEX idx_minutes_sections_verified ON minutes_sections(verified);
|
||||
|
||||
-- 4. 코멘트 추가
|
||||
COMMENT ON TABLE minutes_sections IS '참석자별 회의록 안건 섹션 - AI 통합 회의록 생성 입력 데이터';
|
||||
COMMENT ON COLUMN minutes_sections.id IS '섹션 고유 ID';
|
||||
COMMENT ON COLUMN minutes_sections.minutes_id IS '참석자별 회의록 ID (minutes.id 참조)';
|
||||
COMMENT ON COLUMN minutes_sections.type IS '섹션 타입 (AGENDA: 안건, DISCUSSION: 논의사항, DECISION: 결정사항 등)';
|
||||
COMMENT ON COLUMN minutes_sections.title IS '섹션 제목';
|
||||
COMMENT ON COLUMN minutes_sections.content IS '섹션 내용 (참석자가 작성한 메모)';
|
||||
COMMENT ON COLUMN minutes_sections."order" IS '섹션 순서';
|
||||
COMMENT ON COLUMN minutes_sections.verified IS '검증 완료 여부';
|
||||
COMMENT ON COLUMN minutes_sections.locked IS '편집 잠금 여부';
|
||||
COMMENT ON COLUMN minutes_sections.locked_by IS '잠금 설정한 사용자 ID';
|
||||
|
||||
-- 5. updated_at 자동 업데이트 트리거
|
||||
DROP TRIGGER IF EXISTS update_minutes_sections_updated_at ON minutes_sections;
|
||||
CREATE TRIGGER update_minutes_sections_updated_at
|
||||
BEFORE UPDATE ON minutes_sections
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
Reference in New Issue
Block a user