Chore: 회의록 목록 조회 API 실제 데이터 연동

This commit is contained in:
cyjadela 2025-10-27 17:43:04 +09:00
parent e5337385f4
commit 45dc77cddf
16 changed files with 5129 additions and 44 deletions

File diff suppressed because it is too large Load Diff

View File

@ -116,6 +116,11 @@ public class MinutesDTO {
*/ */
private final Integer completedTodoCount; private final Integer completedTodoCount;
/**
* 참석자
*/
private final Integer participantCount;
/** /**
* 회의 정보 * 회의 정보
*/ */

View File

@ -44,6 +44,7 @@ public class MinutesService implements
private final MinutesSectionReader minutesSectionReader; private final MinutesSectionReader minutesSectionReader;
private final MinutesSectionWriter minutesSectionWriter; private final MinutesSectionWriter minutesSectionWriter;
private final CacheService cacheService; private final CacheService cacheService;
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader participantReader;
/** /**
* 회의록 생성 * 회의록 생성
@ -317,6 +318,29 @@ public class MinutesService implements
* Minutes 도메인을 MinutesDTO로 변환 * Minutes 도메인을 MinutesDTO로 변환
*/ */
private MinutesDTO convertToMinutesDTO(Minutes minutes) { private MinutesDTO convertToMinutesDTO(Minutes minutes) {
// 회의 정보 조회
String meetingTitle = "회의 제목 없음";
try {
Meeting meeting = meetingReader.findById(minutes.getMeetingId()).orElse(null);
if (meeting != null) {
meetingTitle = meeting.getTitle();
}
} catch (Exception e) {
log.warn("회의 정보 조회 실패 - meetingId: {}", minutes.getMeetingId(), e);
}
// TODO 정보는 추후 구현 (현재는 기본값)
int todoCount = 0;
int completedTodoCount = 0;
// 참석자 계산 (모든 참석자)
int participantCount = 0;
try {
participantCount = participantReader.countParticipantsByMeetingId(minutes.getMeetingId());
} catch (Exception e) {
log.warn("참석자 수 계산 실패 - meetingId: {}", minutes.getMeetingId(), e);
}
return MinutesDTO.builder() return MinutesDTO.builder()
.minutesId(minutes.getMinutesId()) .minutesId(minutes.getMinutesId())
.meetingId(minutes.getMeetingId()) .meetingId(minutes.getMeetingId())
@ -327,11 +351,11 @@ public class MinutesService implements
.lastModifiedAt(minutes.getLastModifiedAt()) .lastModifiedAt(minutes.getLastModifiedAt())
.createdBy(minutes.getCreatedBy()) .createdBy(minutes.getCreatedBy())
.lastModifiedBy(minutes.getLastModifiedBy()) .lastModifiedBy(minutes.getLastModifiedBy())
// 추가 필드들은 임시로 기본값 설정 .meetingTitle(meetingTitle)
.meetingTitle("임시 회의 제목") .todoCount(todoCount)
.todoCount(0) .completedTodoCount(completedTodoCount)
.completedTodoCount(0) .participantCount(participantCount)
.memo("") .memo("") // 메모 필드는 추후 구현
.build(); .build();
} }
} }

View File

@ -21,4 +21,9 @@ public interface ParticipantReader {
* 특정 회의에 특정 사용자가 참석자로 등록되어 있는지 확인 * 특정 회의에 특정 사용자가 참석자로 등록되어 있는지 확인
*/ */
boolean existsParticipant(String meetingId, String userId); boolean existsParticipant(String meetingId, String userId);
/**
* 회의 ID로 참석자 조회 (모든 참석자)
*/
int countParticipantsByMeetingId(String meetingId);
} }

View File

@ -2,6 +2,7 @@ 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.Minutes;
import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO; import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO;
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;
@ -16,6 +17,7 @@ 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;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@ -69,40 +71,37 @@ public class MinutesController {
userId, page, size, status, participationType, search); userId, page, size, status, participationType, search);
try { try {
// Mock 데이터 생성 (프론트엔드 테스트용) // 정렬 페이징 설정
List<MinutesListResponse.MinutesItem> mockMinutes = createMockMinutesList(userId); Sort sort = createSort(sortBy, sortDir);
Pageable pageable = PageRequest.of(page, size, sort);
// 필터링 적용 // 실제 데이터 조회
List<MinutesListResponse.MinutesItem> filteredMinutes = mockMinutes.stream() Page<MinutesDTO> minutesPage = minutesService.getMinutesListByUserId(userId, pageable);
// DTO를 Response 형식으로 변환
List<MinutesListResponse.MinutesItem> minutesList = minutesPage.getContent().stream()
.map(this::convertToMinutesItem)
.collect(Collectors.toList());
// 필터링 적용 (상태별)
List<MinutesListResponse.MinutesItem> filteredMinutes = minutesList.stream()
.filter(item -> filterByStatus(item, status)) .filter(item -> filterByStatus(item, status))
.filter(item -> filterByParticipationType(item, participationType, userId))
.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);
// 페이징 적용
int startIndex = page * size;
int endIndex = Math.min(startIndex + size, filteredMinutes.size());
List<MinutesListResponse.MinutesItem> pagedMinutes =
startIndex < filteredMinutes.size() ?
filteredMinutes.subList(startIndex, endIndex) :
List.of();
// 통계 계산
MinutesListResponse.Statistics stats = calculateStatistics(mockMinutes, participationType, userId);
MinutesListResponse response = MinutesListResponse.builder() MinutesListResponse response = MinutesListResponse.builder()
.minutesList(pagedMinutes) .minutesList(filteredMinutes)
.totalCount(filteredMinutes.size()) .totalCount((int) minutesPage.getTotalElements())
.currentPage(page) .currentPage(page)
.totalPages((int) Math.ceil((double) filteredMinutes.size() / size)) .totalPages(minutesPage.getTotalPages())
.statistics(stats) .statistics(stats)
.build(); .build();
log.info("회의록 목록 조회 성공 - userId: {}, total: {}, filtered: {}, paged: {}", log.info("회의록 목록 조회 성공 - userId: {}, total: {}, filtered: {}",
userId, mockMinutes.size(), filteredMinutes.size(), pagedMinutes.size()); userId, minutesPage.getTotalElements(), filteredMinutes.size());
return ResponseEntity.ok(ApiResponse.success(response)); return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) { } catch (Exception e) {
@ -126,13 +125,14 @@ public class MinutesController {
log.info("회의록 상세 조회 요청 - userId: {}, minutesId: {}", userId, minutesId); log.info("회의록 상세 조회 요청 - userId: {}, minutesId: {}", userId, minutesId);
try { try {
// Mock 데이터 생성 (프론트엔드 테스트용) // 실제 데이터 조회
MinutesDetailResponse response = createMockMinutesDetail(minutesId, userId); MinutesDTO minutesDTO = minutesService.getMinutesById(minutesId);
MinutesDetailResponse response = convertToMinutesDetailResponse(minutesDTO);
// 캐시 저장 // 캐시 저장
cacheService.cacheMinutesDetail(minutesId, response); cacheService.cacheMinutesDetail(minutesId, response);
log.info("회의록 상세 조회 성공 (Mock) - minutesId: {}", minutesId); log.info("회의록 상세 조회 성공 - minutesId: {}", minutesId);
return ResponseEntity.ok(ApiResponse.success(response)); return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) { } catch (Exception e) {
@ -326,7 +326,60 @@ public class MinutesController {
} }
// Helper methods // Helper methods
/**
* 정렬 옵션 생성
*/
private Sort createSort(String sortBy, String sortDir) {
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) ? Sort.Direction.ASC : Sort.Direction.DESC;
switch (sortBy) {
case "title":
return Sort.by(direction, "title");
case "meeting":
return Sort.by(direction, "createdAt"); // 회의 일시로 정렬 (임시로 생성일시 사용)
case "modified":
default:
return Sort.by(direction, "lastModifiedAt");
}
}
/**
* 실제 통계 계산
*/
private MinutesListResponse.Statistics calculateRealStatistics(String userId, String participationType) {
try {
// 전체 회의록 조회 (작성자 기준)
List<Minutes> allMinutes = minutesService.getMinutesByCreator(userId);
long totalCount = allMinutes.size();
long draftCount = allMinutes.stream()
.filter(m -> "DRAFT".equals(m.getStatus()))
.count();
long completeCount = allMinutes.stream()
.filter(m -> "FINALIZED".equals(m.getStatus()))
.count();
return MinutesListResponse.Statistics.builder()
.totalCount(totalCount)
.draftCount(draftCount)
.completeCount(completeCount)
.build();
} catch (Exception e) {
log.warn("통계 계산 실패, 기본값 반환 - userId: {}", userId, e);
return MinutesListResponse.Statistics.builder()
.totalCount(0L)
.draftCount(0L)
.completeCount(0L)
.build();
}
}
private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO) { private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO) {
// 완료율 계산
int completionRate = minutesDTO.getTodoCount() > 0 ?
(minutesDTO.getCompletedTodoCount() * 100) / minutesDTO.getTodoCount() : 100;
return MinutesListResponse.MinutesItem.builder() return MinutesListResponse.MinutesItem.builder()
.minutesId(minutesDTO.getMinutesId()) .minutesId(minutesDTO.getMinutesId())
.title(minutesDTO.getTitle()) .title(minutesDTO.getTitle())
@ -335,10 +388,14 @@ public class MinutesController {
.version(minutesDTO.getVersion()) .version(minutesDTO.getVersion())
.createdAt(minutesDTO.getCreatedAt()) .createdAt(minutesDTO.getCreatedAt())
.lastModifiedAt(minutesDTO.getLastModifiedAt()) .lastModifiedAt(minutesDTO.getLastModifiedAt())
.meetingDate(minutesDTO.getCreatedAt()) // 임시로 생성일시 사용
.createdBy(minutesDTO.getCreatedBy()) .createdBy(minutesDTO.getCreatedBy())
.lastModifiedBy(minutesDTO.getLastModifiedBy()) .lastModifiedBy(minutesDTO.getLastModifiedBy())
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 0)
.todoCount(minutesDTO.getTodoCount()) .todoCount(minutesDTO.getTodoCount())
.completedTodoCount(minutesDTO.getCompletedTodoCount()) .completedTodoCount(minutesDTO.getCompletedTodoCount())
.completionRate(completionRate)
.isCreatedByUser(true) // 현재는 작성자 기준으로만 조회하므로 true
.build(); .build();
} }
@ -497,18 +554,10 @@ public class MinutesController {
} }
/** /**
* 참여 유형별 필터링 * 참여 유형별 필터링 - 현재는 사용하지 않음 (작성자 기준으로만 조회)
*/ */
private boolean filterByParticipationType(MinutesListResponse.MinutesItem item, String participationType, String userId) { private boolean filterByParticipationType(MinutesListResponse.MinutesItem item, String participationType, String userId) {
if (participationType == null || participationType.isEmpty()) { // 현재는 작성자 기준으로만 조회하므로 항상 true 반환
return true;
}
if ("created".equals(participationType)) {
return item.isCreatedByUser();
}
if ("attended".equals(participationType)) {
return !item.isCreatedByUser();
}
return true; return true;
} }
@ -870,8 +919,59 @@ public class MinutesController {
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) { private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
// Mock 데이터로 대체 (프로토타입용) // 기본 회의록 정보는 실제 데이터 사용
return createMockMinutesDetail(minutesDTO.getMinutesId(), "user123"); MinutesDetailResponse.MeetingInfo meetingInfo = MinutesDetailResponse.MeetingInfo.builder()
.meetingId(minutesDTO.getMeetingId())
.title(minutesDTO.getMeetingTitle())
.location("회의실 정보 없음") // 추후 실제 데이터로 변경 필요
.participants(List.of()) // 추후 실제 참석자 정보로 변경 필요
.build();
MinutesDetailResponse.Statistics stats = MinutesDetailResponse.Statistics.builder()
.participantCount(minutesDTO.getParticipantCount() != null ? minutesDTO.getParticipantCount() : 0)
.durationMinutes(90) // 기본값 - 추후 실제 데이터로 변경 필요
.agendaCount(0) // 기본값 - 추후 실제 데이터로 변경 필요
.todoCount(minutesDTO.getTodoCount() != null ? minutesDTO.getTodoCount() : 0)
.build();
MinutesDetailResponse.DashboardInfo dashboardInfo = MinutesDetailResponse.DashboardInfo.builder()
.keyPoints(List.of()) // 추후 실제 데이터로 변경 필요
.keywords(List.of()) // 추후 실제 데이터로 변경 필요
.stats(stats)
.decisions(List.of()) // 추후 실제 데이터로 변경 필요
.todoProgress(MinutesDetailResponse.TodoProgress.builder()
.totalCount(minutesDTO.getTodoCount() != null ? minutesDTO.getTodoCount() : 0)
.completedCount(minutesDTO.getCompletedTodoCount() != null ? minutesDTO.getCompletedTodoCount() : 0)
.progressPercentage(calculateProgressPercentage(minutesDTO.getTodoCount(), minutesDTO.getCompletedTodoCount()))
.todos(List.of()) // 추후 실제 데이터로 변경 필요
.build())
.relatedMinutes(List.of()) // 추후 실제 데이터로 변경 필요
.build();
return MinutesDetailResponse.builder()
.minutesId(minutesDTO.getMinutesId())
.title(minutesDTO.getTitle())
.memo(minutesDTO.getMemo() != null ? minutesDTO.getMemo() : "")
.status(minutesDTO.getStatus())
.version(minutesDTO.getVersion())
.createdAt(minutesDTO.getCreatedAt())
.lastModifiedAt(minutesDTO.getLastModifiedAt())
.createdBy(minutesDTO.getCreatedBy())
.lastModifiedBy(minutesDTO.getLastModifiedBy())
.meeting(meetingInfo)
.dashboard(dashboardInfo)
.agendas(List.of()) // 추후 실제 안건 데이터로 변경 필요
.build();
}
private int calculateProgressPercentage(Integer totalCount, Integer completedCount) {
if (totalCount == null || totalCount == 0) {
return 100;
}
if (completedCount == null) {
return 0;
}
return (completedCount * 100) / totalCount;
} }
} }

View File

@ -45,6 +45,12 @@ public class ParticipantGateway implements ParticipantReader, ParticipantWriter
return participantRepository.existsByMeetingIdAndUserId(meetingId, userId); return participantRepository.existsByMeetingIdAndUserId(meetingId, userId);
} }
@Override
@Transactional(readOnly = true)
public int countParticipantsByMeetingId(String meetingId) {
return participantRepository.countByMeetingId(meetingId);
}
@Override @Override
@Transactional @Transactional
public void saveParticipant(String meetingId, String userId) { public void saveParticipant(String meetingId, String userId) {

View File

@ -42,4 +42,9 @@ public interface MeetingParticipantJpaRepository extends JpaRepository<MeetingPa
* 회의 ID와 사용자 ID로 참석자 존재 여부 확인 * 회의 ID와 사용자 ID로 참석자 존재 여부 확인
*/ */
boolean existsByMeetingIdAndUserId(String meetingId, String userId); boolean existsByMeetingIdAndUserId(String meetingId, String userId);
/**
* 회의 ID로 참석자 조회 (모든 참석자)
*/
int countByMeetingId(String meetingId);
} }