Merge pull request #64 from hwanny1128/fix/dashboard

Fix: 회의록 목록 조회 API 수정
This commit is contained in:
Cho Yoon Jin 2025-10-31 13:15:06 +09:00 committed by GitHub
commit 3d6742505a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 3808 additions and 202 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,12 @@ package com.unicorn.hgzero.meeting.infra.controller;
import com.unicorn.hgzero.common.dto.ApiResponse;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.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.Todo;
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO;
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
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.infra.dto.request.UpdateMinutesRequest;
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.gateway.AiServiceGateway;
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.MinutesSectionDTO;
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
@ -40,17 +36,10 @@ import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
@ -69,7 +58,6 @@ public class MinutesController {
private final CacheService cacheService;
private final EventPublisher eventPublisher;
private final MeetingService meetingService;
private final TodoService todoService;
private final AiServiceGateway aiServiceGateway;
private final AgendaSectionService agendaSectionService;
private final ParticipantReader participantReader;
@ -119,6 +107,9 @@ public class MinutesController {
.filter(item -> filterByParticipationType(item, participationType))
.filter(item -> filterBySearch(item, search))
.collect(Collectors.toList());
// 필터링 정렬 적용 (프론트엔드 정렬과 일치)
applySorting(filteredMinutes, sortBy, sortDir);
// 통계 계산 (전체 데이터 기준)
MinutesListResponse.Statistics stats = calculateRealStatistics(userId, participationType);
@ -478,7 +469,8 @@ public class MinutesController {
case "title":
return Sort.by(direction, "title");
case "meeting":
return Sort.by(direction, "createdAt"); // 회의 일시로 정렬 (임시로 생성일시 사용)
// 회의 일시로 정렬 - DB에 meetingDate 필드가 없으므로 createdAt 사용
return Sort.by(direction, "createdAt");
case "modified":
default:
return Sort.by(direction, "lastModifiedAt");
@ -531,16 +523,38 @@ public class MinutesController {
private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO, String userId) {
// 검증완료율 계산 (작성중 상태일 때는 verificationRate 사용, 확정완료시 100%)
int completionRate;
int verificationRate;
if ("DRAFT".equals(minutesDTO.getStatus()) && minutesDTO.getVerificationRate() != null) {
completionRate = minutesDTO.getVerificationRate();
verificationRate = minutesDTO.getVerificationRate();
} else if ("FINALIZED".equals(minutesDTO.getStatus())) {
completionRate = 100;
verificationRate = 100;
} else {
// 기본값 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()
.minutesId(minutesDTO.getMinutesId())
.title(minutesDTO.getTitle())
@ -549,14 +563,16 @@ public class MinutesController {
.version(minutesDTO.getVersion())
.createdAt(minutesDTO.getCreatedAt())
.lastModifiedAt(minutesDTO.getLastModifiedAt())
.meetingDate(minutesDTO.getCreatedAt()) // 임시로 생성일시 사용
.meetingDate(meetingDateTime)
.meetingTime(meetingTime)
.createdBy(minutesDTO.getCreatedBy())
.createdByName(creatorName)
.lastModifiedBy(minutesDTO.getLastModifiedBy())
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 0)
.todoCount(minutesDTO.getTodoCount())
.completedTodoCount(minutesDTO.getCompletedTodoCount())
.completionRate(completionRate)
.isCreatedByUser(minutesDTO.getCreatedBy().equals(userId)) // 현재 사용자가 생성자인지 확인
.verificationRate(verificationRate)
.isCreator(minutesDTO.getCreatedBy().equals(userId)) // 현재 사용자가 생성자인지 확인
.build();
}
@ -585,9 +601,9 @@ public class MinutesController {
}
if ("created".equals(participationType)) {
return item.isCreatedByUser(); // 사용자가 생성한 회의록만
return item.isCreator(); // 사용자가 생성한 회의록만
} else if ("attended".equals(participationType)) {
return !item.isCreatedByUser(); // 사용자가 참여만 회의록
return !item.isCreator(); // 사용자가 참여만 회의록
}
return true;
@ -606,7 +622,7 @@ public class MinutesController {
}
/**
* 정렬 적용
* 정렬 적용 (필터링 )
*/
private void applySorting(List<MinutesListResponse.MinutesItem> items, String sortBy, String sortDir) {
boolean ascending = "asc".equalsIgnoreCase(sortDir);
@ -618,78 +634,20 @@ public class MinutesController {
b.getTitle().compareTo(a.getTitle()));
break;
case "meeting":
// 회의 날짜순 정렬 (최근회의순은 desc가 기본)
items.sort((a, b) -> ascending ?
a.getMeetingDate().compareTo(b.getMeetingDate()) :
b.getMeetingDate().compareTo(a.getMeetingDate()));
break;
case "modified":
default:
// 최근수정순 정렬 (desc가 기본)
items.sort((a, b) -> ascending ?
a.getLastModifiedAt().compareTo(b.getLastModifiedAt()) :
b.getLastModifiedAt().compareTo(a.getLastModifiedAt()));
break;
}
}
/**
* 통계 계산
*/
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) {
try {
@ -951,11 +909,6 @@ public class MinutesController {
.build();
}
private int calculateActualDuration(Object meeting) {
// TODO: 실제 회의 시간 계산 로직 구현
// Meeting 객체에서 startedAt, endedAt을 사용하여 계산
return 90;
}
private MinutesDetailResponse.AgendaInfo convertToAgendaInfo(Object section) {
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) {
// 안건별 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) {
// TODO: AI를 통한 키워드 추출 로직 구현
@ -1261,41 +1137,6 @@ public class MinutesController {
.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);
}
}
/**
* 실제 회의 시간 계산

View File

@ -46,12 +46,32 @@ public class MinutesListResponse {
private LocalDateTime createdAt;
private LocalDateTime lastModifiedAt;
private LocalDateTime meetingDate; // 회의 일시
private String meetingTime; // 회의 시간 (HH:mm 형식)
private String createdBy;
private String createdByName; // 생성자 이름
private String lastModifiedBy;
private int participantCount; // 참석자
private int todoCount;
private int completedTodoCount;
private int completionRate; // 검증완료율
private boolean isCreatedByUser; // 사용자가 생성한 회의록 여부
private int verificationRate; // 검증완료율 (프로토타입과 일치)
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 "";
}
}
}