mirror of
https://github.com/hwanny1128/HGZero.git
synced 2025-12-06 09:06:24 +00:00
Compare commits
6 Commits
5515909206
...
de9f88ff0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de9f88ff0c | ||
|
|
3d6742505a | ||
|
|
44f02a2cc6 | ||
|
|
de6c68d4d1 | ||
|
|
a2ef408a85 | ||
|
|
c4bd8064ec |
13
README.md
13
README.md
@ -13,15 +13,14 @@ HGZero는 업무지식이 부족한 회의록 작성자도 누락 없이 정확
|
|||||||
- **실시간 협업**: WebSocket 기반 실시간 회의록 편집 및 동기화
|
- **실시간 협업**: WebSocket 기반 실시간 회의록 편집 및 동기화
|
||||||
|
|
||||||
### 1.2 MVP 산출물
|
### 1.2 MVP 산출물
|
||||||
- **발표자료**: {발표자료 링크}
|
- **발표자료**: [AI 기반 회의록 작성 서비스](docs/(MVP)%20AI%20기반%20회의록%20작성%20서비스_v1.11.pdf)
|
||||||
- **설계결과**:
|
- **설계결과**:
|
||||||
- [유저스토리](design/userstory.md)
|
- [유저스토리](design/userstory.md)
|
||||||
- [논리 아키텍처](design/backend/logical/logical-architecture.md)
|
- [논리 아키텍처](design/backend/logical/logical-architecture.md)
|
||||||
- [API 설계서](design/backend/api/API설계서.md)
|
- [API 설계서](design/backend/api/API설계서.md)
|
||||||
- **Git Repo**:
|
- **Git Repo**:
|
||||||
- **메인**: https://gitea.cbiz.kubepia.net/shared-dg05-coffeeQuokka/hgzero.git
|
- **메인**: https://gitea.cbiz.kubepia.net/shared-dg05-coffeeQuokka/hgzero.git
|
||||||
- **프론트엔드**: {프론트엔드 Repository 링크}
|
|
||||||
- **manifest**: {Manifest Repository 링크}
|
|
||||||
- **시연 동영상**: {시연 동영상 링크}
|
- **시연 동영상**: {시연 동영상 링크}
|
||||||
|
|
||||||
## 2. 시스템 아키텍처
|
## 2. 시스템 아키텍처
|
||||||
@ -31,13 +30,13 @@ HGZero는 업무지식이 부족한 회의록 작성자도 누락 없이 정확
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ Frontend │
|
│ Frontend │
|
||||||
│ React 18 + TypeScript │
|
│ React 18 + TypeScript │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ NGINX Ingress │
|
│ NGINX Ingress │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌─────────────────────┼─────────────────────┐
|
┌─────────────────────┼─────────────────────┐
|
||||||
@ -362,7 +361,7 @@ kubectl get ingress
|
|||||||
- Notification Service: http://{INGRESS_URL}/notification/swagger-ui.html
|
- Notification Service: http://{INGRESS_URL}/notification/swagger-ui.html
|
||||||
|
|
||||||
#### 3) 로그인 테스트
|
#### 3) 로그인 테스트
|
||||||
- ID: user-005, user2@example.com
|
- ID: meeting-test
|
||||||
- PW: 8자리
|
- PW: 8자리
|
||||||
|
|
||||||
## 5. 팀
|
## 5. 팀
|
||||||
|
|||||||
BIN
docs/(MVP) AI 기반 회의록 작성 서비스_v1.11.pdf
Normal file
BIN
docs/(MVP) AI 기반 회의록 작성 서비스_v1.11.pdf
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -41,5 +41,6 @@ public class MeetingEndDTO {
|
|||||||
@Builder
|
@Builder
|
||||||
public static class TodoSummaryDTO {
|
public static class TodoSummaryDTO {
|
||||||
private final String title;
|
private final String title;
|
||||||
|
private final String assignee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,8 +124,8 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
meetingRepository.save(meeting);
|
meetingRepository.save(meeting);
|
||||||
log.info("회의 상태 업데이트 완료 - status: {}", meeting.getStatus());
|
log.info("회의 상태 업데이트 완료 - status: {}", meeting.getStatus());
|
||||||
|
|
||||||
// 9. 응답 DTO 생성
|
// 9. 응답 DTO 생성 (AI 응답의 todos를 그대로 사용)
|
||||||
MeetingEndDTO result = createMeetingEndDTO(meeting, analysis, todos, participantMinutesList.size());
|
MeetingEndDTO result = createMeetingEndDTO(meeting, aiResponse, todos.size(), participantMinutesList.size());
|
||||||
|
|
||||||
log.info("회의 종료 처리 완료 - meetingId: {}, 안건 수: {}, Todo 수: {}",
|
log.info("회의 종료 처리 완료 - meetingId: {}, 안건 수: {}, Todo 수: {}",
|
||||||
meetingId, analysis.getAgendaAnalyses().size(), todos.size());
|
meetingId, analysis.getAgendaAnalyses().size(), todos.size());
|
||||||
@ -308,7 +308,8 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
private List<TodoEntity> createAndSaveTodos(MeetingEntity meeting, ConsolidateResponse aiResponse, MeetingAnalysis analysis) {
|
private List<TodoEntity> createAndSaveTodos(MeetingEntity meeting, ConsolidateResponse aiResponse, MeetingAnalysis analysis) {
|
||||||
List<TodoEntity> todos = aiResponse.getAgendaSummaries().stream()
|
List<TodoEntity> todos = aiResponse.getAgendaSummaries().stream()
|
||||||
.<TodoEntity>flatMap(agenda -> {
|
.<TodoEntity>flatMap(agenda -> {
|
||||||
// agendaId는 향후 Todo와 안건 매핑에 사용될 수 있음 (현재는 사용하지 않음)
|
// 안건 번호를 description에 저장하여 나중에 필터링에 사용
|
||||||
|
Integer agendaNumber = agenda.getAgendaNumber();
|
||||||
List<ExtractedTodoDTO> todoList = agenda.getTodos() != null ? agenda.getTodos() : List.of();
|
List<ExtractedTodoDTO> todoList = agenda.getTodos() != null ? agenda.getTodos() : List.of();
|
||||||
return todoList.stream()
|
return todoList.stream()
|
||||||
.<TodoEntity>map(todo -> TodoEntity.builder()
|
.<TodoEntity>map(todo -> TodoEntity.builder()
|
||||||
@ -316,6 +317,7 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
.meetingId(meeting.getMeetingId())
|
.meetingId(meeting.getMeetingId())
|
||||||
.minutesId(meeting.getMeetingId()) // 실제로는 minutesId 필요
|
.minutesId(meeting.getMeetingId()) // 실제로는 minutesId 필요
|
||||||
.title(todo.getTitle())
|
.title(todo.getTitle())
|
||||||
|
.description("안건" + agendaNumber) // 안건 번호를 description에 임시 저장
|
||||||
.assigneeId(todo.getAssignee() != null ? todo.getAssignee() : "") // AI가 추출한 담당자
|
.assigneeId(todo.getAssignee() != null ? todo.getAssignee() : "") // AI가 추출한 담당자
|
||||||
.status("PENDING")
|
.status("PENDING")
|
||||||
.build());
|
.build());
|
||||||
@ -342,33 +344,36 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 회의 종료 결과 DTO 생성
|
* 회의 종료 결과 DTO 생성 (AI 응답 직접 사용)
|
||||||
*/
|
*/
|
||||||
private MeetingEndDTO createMeetingEndDTO(MeetingEntity meeting, MeetingAnalysis analysis,
|
private MeetingEndDTO createMeetingEndDTO(MeetingEntity meeting, ConsolidateResponse aiResponse,
|
||||||
List<TodoEntity> todos, int participantCount) {
|
int todoCount, int participantCount) {
|
||||||
// 회의 소요 시간 계산
|
// 회의 소요 시간 계산
|
||||||
int durationMinutes = calculateDurationMinutes(meeting.getStartedAt(), meeting.getEndedAt());
|
int durationMinutes = calculateDurationMinutes(meeting.getStartedAt(), meeting.getEndedAt());
|
||||||
|
|
||||||
// 안건별 요약 DTO 생성
|
// AI 응답의 안건 정보를 그대로 DTO로 변환 (todos 포함)
|
||||||
List<MeetingEndDTO.AgendaSummaryDTO> agendaSummaries = analysis.getAgendaAnalyses().stream()
|
List<MeetingEndDTO.AgendaSummaryDTO> agendaSummaries = aiResponse.getAgendaSummaries().stream()
|
||||||
.<MeetingEndDTO.AgendaSummaryDTO>map(agenda -> {
|
.<MeetingEndDTO.AgendaSummaryDTO>map(agenda -> {
|
||||||
// 해당 안건의 Todo 필터링 (agendaId가 없을 수 있음)
|
// 안건별 todos 변환
|
||||||
List<MeetingEndDTO.TodoSummaryDTO> agendaTodos = todos.stream()
|
List<MeetingEndDTO.TodoSummaryDTO> todoList = new ArrayList<>();
|
||||||
.filter(todo -> agenda.getAgendaId().equals(todo.getMinutesId())) // 임시 매핑
|
if (agenda.getTodos() != null) {
|
||||||
.<MeetingEndDTO.TodoSummaryDTO>map(todo -> MeetingEndDTO.TodoSummaryDTO.builder()
|
for (ExtractedTodoDTO todo : agenda.getTodos()) {
|
||||||
|
todoList.add(MeetingEndDTO.TodoSummaryDTO.builder()
|
||||||
.title(todo.getTitle())
|
.title(todo.getTitle())
|
||||||
.build())
|
.assignee(todo.getAssignee())
|
||||||
.collect(Collectors.toList());
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return MeetingEndDTO.AgendaSummaryDTO.builder()
|
return MeetingEndDTO.AgendaSummaryDTO.builder()
|
||||||
.title(agenda.getTitle())
|
.title(agenda.getAgendaTitle())
|
||||||
.aiSummaryShort(agenda.getAiSummaryShort())
|
.aiSummaryShort(agenda.getSummaryShort())
|
||||||
.details(MeetingEndDTO.AgendaDetailsDTO.builder()
|
.details(MeetingEndDTO.AgendaDetailsDTO.builder()
|
||||||
.discussion(agenda.getDiscussion())
|
.discussion(agenda.getSummary())
|
||||||
.decisions(agenda.getDecisions())
|
.decisions(agenda.getDecisions())
|
||||||
.pending(agenda.getPending())
|
.pending(agenda.getPending())
|
||||||
.build())
|
.build())
|
||||||
.todos(agendaTodos)
|
.todos(todoList)
|
||||||
.build();
|
.build();
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
@ -377,9 +382,9 @@ public class EndMeetingService implements EndMeetingUseCase {
|
|||||||
.title(meeting.getTitle())
|
.title(meeting.getTitle())
|
||||||
.participantCount(participantCount)
|
.participantCount(participantCount)
|
||||||
.durationMinutes(durationMinutes)
|
.durationMinutes(durationMinutes)
|
||||||
.agendaCount(analysis.getAgendaAnalyses().size())
|
.agendaCount(aiResponse.getAgendaSummaries().size())
|
||||||
.todoCount(todos.size())
|
.todoCount(todoCount)
|
||||||
.keywords(analysis.getKeywords())
|
.keywords(aiResponse.getKeywords())
|
||||||
.agendaSummaries(agendaSummaries)
|
.agendaSummaries(agendaSummaries)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,12 @@ 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.common.exception.BusinessException;
|
import com.unicorn.hgzero.common.exception.BusinessException;
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
|
||||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||||
import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO;
|
import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO;
|
||||||
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
|
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
|
||||||
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
|
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
|
||||||
import com.unicorn.hgzero.meeting.biz.service.MinutesSectionService;
|
import com.unicorn.hgzero.meeting.biz.service.MinutesSectionService;
|
||||||
import com.unicorn.hgzero.meeting.biz.service.TodoService;
|
|
||||||
import com.unicorn.hgzero.meeting.biz.service.AgendaSectionService;
|
import com.unicorn.hgzero.meeting.biz.service.AgendaSectionService;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateMinutesRequest;
|
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateMinutesRequest;
|
||||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateAgendaSectionsRequest;
|
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateAgendaSectionsRequest;
|
||||||
@ -21,7 +18,6 @@ import com.unicorn.hgzero.meeting.infra.cache.CacheService;
|
|||||||
import com.unicorn.hgzero.meeting.infra.event.publisher.EventPublisher;
|
import com.unicorn.hgzero.meeting.infra.event.publisher.EventPublisher;
|
||||||
import com.unicorn.hgzero.meeting.infra.gateway.AiServiceGateway;
|
import com.unicorn.hgzero.meeting.infra.gateway.AiServiceGateway;
|
||||||
import com.unicorn.hgzero.meeting.biz.dto.AiAnalysisDTO;
|
import com.unicorn.hgzero.meeting.biz.dto.AiAnalysisDTO;
|
||||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesAnalysisRequestEvent;
|
|
||||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesFinalizedEvent;
|
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesFinalizedEvent;
|
||||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesSectionDTO;
|
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesSectionDTO;
|
||||||
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
|
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
|
||||||
@ -40,17 +36,10 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,7 +58,6 @@ public class MinutesController {
|
|||||||
private final CacheService cacheService;
|
private final CacheService cacheService;
|
||||||
private final EventPublisher eventPublisher;
|
private final EventPublisher eventPublisher;
|
||||||
private final MeetingService meetingService;
|
private final MeetingService meetingService;
|
||||||
private final TodoService todoService;
|
|
||||||
private final AiServiceGateway aiServiceGateway;
|
private final AiServiceGateway aiServiceGateway;
|
||||||
private final AgendaSectionService agendaSectionService;
|
private final AgendaSectionService agendaSectionService;
|
||||||
private final ParticipantReader participantReader;
|
private final ParticipantReader participantReader;
|
||||||
@ -120,6 +108,9 @@ public class MinutesController {
|
|||||||
.filter(item -> filterBySearch(item, search))
|
.filter(item -> filterBySearch(item, search))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 필터링 후 정렬 적용 (프론트엔드 정렬과 일치)
|
||||||
|
applySorting(filteredMinutes, sortBy, sortDir);
|
||||||
|
|
||||||
// 통계 계산 (전체 데이터 기준)
|
// 통계 계산 (전체 데이터 기준)
|
||||||
MinutesListResponse.Statistics stats = calculateRealStatistics(userId, participationType);
|
MinutesListResponse.Statistics stats = calculateRealStatistics(userId, participationType);
|
||||||
|
|
||||||
@ -478,7 +469,8 @@ public class MinutesController {
|
|||||||
case "title":
|
case "title":
|
||||||
return Sort.by(direction, "title");
|
return Sort.by(direction, "title");
|
||||||
case "meeting":
|
case "meeting":
|
||||||
return Sort.by(direction, "createdAt"); // 회의 일시로 정렬 (임시로 생성일시 사용)
|
// 회의 일시로 정렬 - DB에 meetingDate 필드가 없으므로 createdAt 사용
|
||||||
|
return Sort.by(direction, "createdAt");
|
||||||
case "modified":
|
case "modified":
|
||||||
default:
|
default:
|
||||||
return Sort.by(direction, "lastModifiedAt");
|
return Sort.by(direction, "lastModifiedAt");
|
||||||
@ -531,16 +523,38 @@ public class MinutesController {
|
|||||||
|
|
||||||
private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO, String userId) {
|
private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO, String userId) {
|
||||||
// 검증완료율 계산 (작성중 상태일 때는 verificationRate 사용, 확정완료시 100%)
|
// 검증완료율 계산 (작성중 상태일 때는 verificationRate 사용, 확정완료시 100%)
|
||||||
int completionRate;
|
int verificationRate;
|
||||||
if ("DRAFT".equals(minutesDTO.getStatus()) && minutesDTO.getVerificationRate() != null) {
|
if ("DRAFT".equals(minutesDTO.getStatus()) && minutesDTO.getVerificationRate() != null) {
|
||||||
completionRate = minutesDTO.getVerificationRate();
|
verificationRate = minutesDTO.getVerificationRate();
|
||||||
} else if ("FINALIZED".equals(minutesDTO.getStatus())) {
|
} else if ("FINALIZED".equals(minutesDTO.getStatus())) {
|
||||||
completionRate = 100;
|
verificationRate = 100;
|
||||||
} else {
|
} else {
|
||||||
// 기본값 0
|
// 기본값 0
|
||||||
completionRate = 0;
|
verificationRate = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 회의 날짜/시간 추출
|
||||||
|
LocalDateTime meetingDateTime = minutesDTO.getCreatedAt(); // 임시로 생성일시 사용
|
||||||
|
String meetingTime = null;
|
||||||
|
|
||||||
|
// 실제 회의 정보에서 날짜/시간 추출 시도
|
||||||
|
try {
|
||||||
|
var meeting = meetingService.getMeeting(minutesDTO.getMeetingId());
|
||||||
|
if (meeting.getScheduledAt() != null) {
|
||||||
|
meetingDateTime = meeting.getScheduledAt();
|
||||||
|
meetingTime = meeting.getScheduledAt().toLocalTime().toString().substring(0, 5);
|
||||||
|
} else if (meeting.getStartedAt() != null) {
|
||||||
|
meetingDateTime = meeting.getStartedAt();
|
||||||
|
meetingTime = meeting.getStartedAt().toLocalTime().toString().substring(0, 5);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("회의 정보 조회 실패, 기본값 사용 - meetingId: {}", minutesDTO.getMeetingId());
|
||||||
|
meetingTime = meetingDateTime.toLocalTime().toString().substring(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 생성자 이름 조회 (임시)
|
||||||
|
String creatorName = getUserName(minutesDTO.getCreatedBy());
|
||||||
|
|
||||||
return MinutesListResponse.MinutesItem.builder()
|
return MinutesListResponse.MinutesItem.builder()
|
||||||
.minutesId(minutesDTO.getMinutesId())
|
.minutesId(minutesDTO.getMinutesId())
|
||||||
.title(minutesDTO.getTitle())
|
.title(minutesDTO.getTitle())
|
||||||
@ -549,14 +563,16 @@ public class MinutesController {
|
|||||||
.version(minutesDTO.getVersion())
|
.version(minutesDTO.getVersion())
|
||||||
.createdAt(minutesDTO.getCreatedAt())
|
.createdAt(minutesDTO.getCreatedAt())
|
||||||
.lastModifiedAt(minutesDTO.getLastModifiedAt())
|
.lastModifiedAt(minutesDTO.getLastModifiedAt())
|
||||||
.meetingDate(minutesDTO.getCreatedAt()) // 임시로 생성일시 사용
|
.meetingDate(meetingDateTime)
|
||||||
|
.meetingTime(meetingTime)
|
||||||
.createdBy(minutesDTO.getCreatedBy())
|
.createdBy(minutesDTO.getCreatedBy())
|
||||||
|
.createdByName(creatorName)
|
||||||
.lastModifiedBy(minutesDTO.getLastModifiedBy())
|
.lastModifiedBy(minutesDTO.getLastModifiedBy())
|
||||||
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 0)
|
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 0)
|
||||||
.todoCount(minutesDTO.getTodoCount())
|
.todoCount(minutesDTO.getTodoCount())
|
||||||
.completedTodoCount(minutesDTO.getCompletedTodoCount())
|
.completedTodoCount(minutesDTO.getCompletedTodoCount())
|
||||||
.completionRate(completionRate)
|
.verificationRate(verificationRate)
|
||||||
.isCreatedByUser(minutesDTO.getCreatedBy().equals(userId)) // 현재 사용자가 생성자인지 확인
|
.isCreator(minutesDTO.getCreatedBy().equals(userId)) // 현재 사용자가 생성자인지 확인
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -585,9 +601,9 @@ public class MinutesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("created".equals(participationType)) {
|
if ("created".equals(participationType)) {
|
||||||
return item.isCreatedByUser(); // 사용자가 생성한 회의록만
|
return item.isCreator(); // 사용자가 생성한 회의록만
|
||||||
} else if ("attended".equals(participationType)) {
|
} else if ("attended".equals(participationType)) {
|
||||||
return !item.isCreatedByUser(); // 사용자가 참여만 한 회의록
|
return !item.isCreator(); // 사용자가 참여만 한 회의록
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -606,7 +622,7 @@ public class MinutesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 정렬 적용
|
* 정렬 적용 (필터링 후)
|
||||||
*/
|
*/
|
||||||
private void applySorting(List<MinutesListResponse.MinutesItem> items, String sortBy, String sortDir) {
|
private void applySorting(List<MinutesListResponse.MinutesItem> items, String sortBy, String sortDir) {
|
||||||
boolean ascending = "asc".equalsIgnoreCase(sortDir);
|
boolean ascending = "asc".equalsIgnoreCase(sortDir);
|
||||||
@ -618,12 +634,14 @@ public class MinutesController {
|
|||||||
b.getTitle().compareTo(a.getTitle()));
|
b.getTitle().compareTo(a.getTitle()));
|
||||||
break;
|
break;
|
||||||
case "meeting":
|
case "meeting":
|
||||||
|
// 회의 날짜순 정렬 (최근회의순은 desc가 기본)
|
||||||
items.sort((a, b) -> ascending ?
|
items.sort((a, b) -> ascending ?
|
||||||
a.getMeetingDate().compareTo(b.getMeetingDate()) :
|
a.getMeetingDate().compareTo(b.getMeetingDate()) :
|
||||||
b.getMeetingDate().compareTo(a.getMeetingDate()));
|
b.getMeetingDate().compareTo(a.getMeetingDate()));
|
||||||
break;
|
break;
|
||||||
case "modified":
|
case "modified":
|
||||||
default:
|
default:
|
||||||
|
// 최근수정순 정렬 (desc가 기본)
|
||||||
items.sort((a, b) -> ascending ?
|
items.sort((a, b) -> ascending ?
|
||||||
a.getLastModifiedAt().compareTo(b.getLastModifiedAt()) :
|
a.getLastModifiedAt().compareTo(b.getLastModifiedAt()) :
|
||||||
b.getLastModifiedAt().compareTo(a.getLastModifiedAt()));
|
b.getLastModifiedAt().compareTo(a.getLastModifiedAt()));
|
||||||
@ -631,66 +649,6 @@ public class MinutesController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 통계 계산
|
|
||||||
*/
|
|
||||||
private MinutesListResponse.Statistics calculateStatistics(List<MinutesListResponse.MinutesItem> allItems,
|
|
||||||
String participationType, String userId) {
|
|
||||||
List<MinutesListResponse.MinutesItem> filteredItems = allItems.stream()
|
|
||||||
.filter(item -> filterByParticipationType(item, participationType))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
long totalCount = filteredItems.size();
|
|
||||||
long draftCount = filteredItems.stream()
|
|
||||||
.filter(item -> "DRAFT".equals(item.getStatus()))
|
|
||||||
.count();
|
|
||||||
long completeCount = filteredItems.stream()
|
|
||||||
.filter(item -> "FINALIZED".equals(item.getStatus()))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
return MinutesListResponse.Statistics.builder()
|
|
||||||
.totalCount(totalCount)
|
|
||||||
.draftCount(draftCount)
|
|
||||||
.completeCount(completeCount)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock 관련회의록 생성 (프로토타입 기반)
|
|
||||||
*/
|
|
||||||
private List<MinutesDetailResponse.RelatedMinutes> createMockRelatedMinutes() {
|
|
||||||
return List.of(
|
|
||||||
MinutesDetailResponse.RelatedMinutes.builder()
|
|
||||||
.minutesId("minutes-related-001")
|
|
||||||
.title("AI 기능 개선 회의")
|
|
||||||
.meetingDate(LocalDateTime.of(2025, 10, 23, 15, 0))
|
|
||||||
.author("이준호")
|
|
||||||
.relevancePercentage(92)
|
|
||||||
.relevanceLevel("HIGH")
|
|
||||||
.summary("AI 요약 정확도 개선 방안 논의. BERT 모델 도입 및 학습 데이터 확보 계획 수립.")
|
|
||||||
.build(),
|
|
||||||
MinutesDetailResponse.RelatedMinutes.builder()
|
|
||||||
.minutesId("minutes-related-002")
|
|
||||||
.title("개발 리소스 계획 회의")
|
|
||||||
.meetingDate(LocalDateTime.of(2025, 10, 22, 11, 0))
|
|
||||||
.author("김민준")
|
|
||||||
.relevancePercentage(88)
|
|
||||||
.relevanceLevel("MEDIUM")
|
|
||||||
.summary("Q4 개발 리소스 현황 및 배분 계획. 신규 프로젝트 우선순위 협의.")
|
|
||||||
.build(),
|
|
||||||
MinutesDetailResponse.RelatedMinutes.builder()
|
|
||||||
.minutesId("minutes-related-003")
|
|
||||||
.title("경쟁사 분석 회의")
|
|
||||||
.meetingDate(LocalDateTime.of(2025, 10, 20, 10, 0))
|
|
||||||
.author("박서연")
|
|
||||||
.relevancePercentage(78)
|
|
||||||
.relevanceLevel("MEDIUM")
|
|
||||||
.summary("경쟁사 A, B, C 분석 결과. 우리의 차별점은 실시간 협업 및 검증 기능.")
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
|
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
|
||||||
try {
|
try {
|
||||||
// 실제 회의 정보 조회
|
// 실제 회의 정보 조회
|
||||||
@ -951,11 +909,6 @@ public class MinutesController {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int calculateActualDuration(Object meeting) {
|
|
||||||
// TODO: 실제 회의 시간 계산 로직 구현
|
|
||||||
// Meeting 객체에서 startedAt, endedAt을 사용하여 계산
|
|
||||||
return 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MinutesDetailResponse.AgendaInfo convertToAgendaInfo(Object section) {
|
private MinutesDetailResponse.AgendaInfo convertToAgendaInfo(Object section) {
|
||||||
if (!(section instanceof MinutesSection)) {
|
if (!(section instanceof MinutesSection)) {
|
||||||
@ -989,38 +942,6 @@ public class MinutesController {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private MinutesDetailResponse.SimpleTodo convertToSimpleTodo(Object todo) {
|
|
||||||
if (!(todo instanceof Todo)) {
|
|
||||||
log.warn("Todo가 아닌 객체가 전달됨: {}", todo.getClass().getSimpleName());
|
|
||||||
return MinutesDetailResponse.SimpleTodo.builder()
|
|
||||||
.todoId("unknown-todo")
|
|
||||||
.title("변환 실패 Todo")
|
|
||||||
.assigneeName("알 수 없음")
|
|
||||||
.status("PENDING")
|
|
||||||
.priority("LOW")
|
|
||||||
.dueDate(LocalDateTime.now().plusDays(7))
|
|
||||||
.dueDayStatus("D-7")
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Todo todoEntity = (Todo) todo;
|
|
||||||
|
|
||||||
// 담당자 이름 조회 (현재는 기본값 사용, 실제로는 User 서비스에서 조회 필요)
|
|
||||||
String assigneeName = getAssigneeName(todoEntity.getAssigneeId());
|
|
||||||
|
|
||||||
// 마감일 상태 계산
|
|
||||||
String dueDayStatus = calculateDueDayStatus(todoEntity.getDueDate(), todoEntity.getStatus());
|
|
||||||
|
|
||||||
return MinutesDetailResponse.SimpleTodo.builder()
|
|
||||||
.todoId(todoEntity.getTodoId())
|
|
||||||
.title(todoEntity.getTitle() != null ? todoEntity.getTitle() : "제목 없음")
|
|
||||||
.assigneeName(assigneeName)
|
|
||||||
.status(todoEntity.getStatus() != null ? todoEntity.getStatus() : "PENDING")
|
|
||||||
.priority(todoEntity.getPriority() != null ? todoEntity.getPriority() : "MEDIUM")
|
|
||||||
.dueDate(todoEntity.getDueDate() != null ? todoEntity.getDueDate().atStartOfDay() : null)
|
|
||||||
.dueDayStatus(dueDayStatus)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<MinutesDetailResponse.KeyPoint> extractKeyPoints(List<MinutesDetailResponse.AgendaInfo> agendas) {
|
private List<MinutesDetailResponse.KeyPoint> extractKeyPoints(List<MinutesDetailResponse.AgendaInfo> agendas) {
|
||||||
// 안건별 AI 요약에서 핵심내용 추출
|
// 안건별 AI 요약에서 핵심내용 추출
|
||||||
@ -1044,51 +965,6 @@ public class MinutesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 담당자 이름 조회 (실제로는 User 서비스에서 조회 필요)
|
|
||||||
*/
|
|
||||||
private String getAssigneeName(String assigneeId) {
|
|
||||||
if (assigneeId == null) {
|
|
||||||
return "미지정";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: 실제 User 서비스에서 사용자 정보 조회
|
|
||||||
// 현재는 간단한 매핑 사용
|
|
||||||
switch (assigneeId) {
|
|
||||||
case "user1":
|
|
||||||
return "김민준";
|
|
||||||
case "user2":
|
|
||||||
return "박서연";
|
|
||||||
case "user3":
|
|
||||||
return "이준호";
|
|
||||||
default:
|
|
||||||
return "사용자" + assigneeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 마감일 상태 계산
|
|
||||||
*/
|
|
||||||
private String calculateDueDayStatus(LocalDate dueDate, String status) {
|
|
||||||
if (dueDate == null) {
|
|
||||||
return "마감일 없음";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("COMPLETED".equals(status)) {
|
|
||||||
return "완료";
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
long daysDiff = ChronoUnit.DAYS.between(today, dueDate);
|
|
||||||
|
|
||||||
if (daysDiff < 0) {
|
|
||||||
return "D+" + Math.abs(daysDiff); // 마감일 지남
|
|
||||||
} else if (daysDiff == 0) {
|
|
||||||
return "D-Day";
|
|
||||||
} else {
|
|
||||||
return "D-" + daysDiff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> extractKeywords(List<MinutesDetailResponse.AgendaInfo> agendas) {
|
private List<String> extractKeywords(List<MinutesDetailResponse.AgendaInfo> agendas) {
|
||||||
// TODO: AI를 통한 키워드 추출 로직 구현
|
// TODO: AI를 통한 키워드 추출 로직 구현
|
||||||
@ -1261,41 +1137,6 @@ public class MinutesController {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 분석 요청 이벤트 발행
|
|
||||||
*/
|
|
||||||
private void publishAiAnalysisRequest(MinutesDTO minutesDTO, String requesterId, String requesterName) {
|
|
||||||
try {
|
|
||||||
// 회의 메타정보 구성
|
|
||||||
MinutesAnalysisRequestEvent.MeetingMeta meetingMeta = MinutesAnalysisRequestEvent.MeetingMeta.builder()
|
|
||||||
.title(minutesDTO.getMeetingTitle())
|
|
||||||
.meetingDate(minutesDTO.getCreatedAt())
|
|
||||||
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 1)
|
|
||||||
.durationMinutes(90) // 기본값
|
|
||||||
.organizerId(minutesDTO.getCreatedBy())
|
|
||||||
.participantIds(new String[]{requesterId}) // 기본값
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// AI 분석 요청 이벤트 생성
|
|
||||||
MinutesAnalysisRequestEvent requestEvent = MinutesAnalysisRequestEvent.create(
|
|
||||||
minutesDTO.getMinutesId(),
|
|
||||||
minutesDTO.getMeetingId(),
|
|
||||||
requesterId,
|
|
||||||
requesterName,
|
|
||||||
extractContentForAiAnalysis(minutesDTO),
|
|
||||||
meetingMeta
|
|
||||||
);
|
|
||||||
|
|
||||||
// 이벤트 발행
|
|
||||||
eventPublisher.publishMinutesAnalysisRequest(requestEvent);
|
|
||||||
|
|
||||||
log.info("AI 분석 요청 이벤트 발행 완료 - minutesId: {}, eventId: {}",
|
|
||||||
minutesDTO.getMinutesId(), requestEvent.getEventId());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("AI 분석 요청 이벤트 발행 실패 - minutesId: {}", minutesDTO.getMinutesId(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 실제 회의 시간 계산
|
* 실제 회의 시간 계산
|
||||||
|
|||||||
@ -46,12 +46,32 @@ public class MinutesListResponse {
|
|||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime lastModifiedAt;
|
private LocalDateTime lastModifiedAt;
|
||||||
private LocalDateTime meetingDate; // 회의 일시
|
private LocalDateTime meetingDate; // 회의 일시
|
||||||
|
private String meetingTime; // 회의 시간 (HH:mm 형식)
|
||||||
private String createdBy;
|
private String createdBy;
|
||||||
|
private String createdByName; // 생성자 이름
|
||||||
private String lastModifiedBy;
|
private String lastModifiedBy;
|
||||||
private int participantCount; // 참석자 수
|
private int participantCount; // 참석자 수
|
||||||
private int todoCount;
|
private int todoCount;
|
||||||
private int completedTodoCount;
|
private int completedTodoCount;
|
||||||
private int completionRate; // 검증완료율
|
private int verificationRate; // 검증완료율 (프로토타입과 일치)
|
||||||
private boolean isCreatedByUser; // 사용자가 생성한 회의록 여부
|
private boolean isCreator; // 현재 사용자가 생성자인지 여부
|
||||||
|
|
||||||
|
// 편의 메서드 추가
|
||||||
|
public String getFormattedDate() {
|
||||||
|
if (meetingDate != null) {
|
||||||
|
return meetingDate.toLocalDate().toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormattedTime() {
|
||||||
|
if (meetingTime != null) {
|
||||||
|
return meetingTime;
|
||||||
|
}
|
||||||
|
if (meetingDate != null) {
|
||||||
|
return meetingDate.toLocalTime().toString().substring(0, 5);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user