Compare commits

..

2 Commits

Author SHA1 Message Date
cyjadela
ec73def9d1 Fix: 대시보드 조회 내 회의록 로직 수정 2025-10-31 12:12:16 +09:00
cyjadela
1024fbd25d Fix: 회의록 목록 조회 수정 중 2025-10-31 11:48:44 +09:00
8 changed files with 3862 additions and 44 deletions

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,12 @@ public class MinutesDTO {
* 참석자 * 참석자
*/ */
private final Integer participantCount; private final Integer participantCount;
/**
* 검증완료율 (작성중 상태일 때만 유효)
* 0-100 사이의
*/
private final Integer verificationRate;
/** /**
* 회의 정보 * 회의 정보

View File

@ -240,20 +240,21 @@ public class MinutesService implements
/** /**
* 사용자 ID로 회의록 목록 조회 (페이징) * 사용자 ID로 회의록 목록 조회 (페이징)
* 사용자가 생성했거나 참여한 회의의 회의록을 모두 조회
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Page<MinutesDTO> getMinutesListByUserId(String userId, Pageable pageable) { public Page<MinutesDTO> getMinutesListByUserId(String userId, Pageable pageable) {
log.debug("Getting minutes list by userId: {}", userId); log.debug("Getting minutes list by userId: {}", userId);
// 여기서는 임시로 작성자 기준으로 조회 (실제로는 참석자나 권한 기반으로 조회해야 ) // 사용자가 생성했거나 참여한 회의의 회의록 조회
List<Minutes> minutesList = minutesReader.findByCreatedBy(userId); List<Minutes> minutesList = minutesReader.findByParticipantUserId(userId);
// Minutes를 MinutesDTO로 변환 // Minutes를 MinutesDTO로 변환
List<MinutesDTO> minutesDTOList = minutesList.stream() List<MinutesDTO> minutesDTOList = minutesList.stream()
.map(this::convertToMinutesDTO) .map(this::convertToMinutesDTO)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 페이징 처리 (임시로 전체 목록 반환) // 페이징 처리
int start = (int) pageable.getOffset(); int start = (int) pageable.getOffset();
int end = Math.min((start + pageable.getPageSize()), minutesDTOList.size()); int end = Math.min((start + pageable.getPageSize()), minutesDTOList.size());
List<MinutesDTO> pageContent = minutesDTOList.subList(start, end); List<MinutesDTO> pageContent = minutesDTOList.subList(start, end);
@ -371,6 +372,15 @@ public class MinutesService implements
log.warn("섹션 정보 변환 실패 - minutesId: {}", minutes.getMinutesId(), e); log.warn("섹션 정보 변환 실패 - minutesId: {}", minutes.getMinutesId(), e);
} }
// 검증완료율 계산 (작성중 상태일 때만)
Integer verificationRate = null;
if ("DRAFT".equals(minutes.getStatus()) && sectionDTOs != null && !sectionDTOs.isEmpty()) {
long verifiedCount = sectionDTOs.stream()
.filter(section -> Boolean.TRUE.equals(section.getIsVerified()))
.count();
verificationRate = (int) ((verifiedCount * 100) / sectionDTOs.size());
}
// decisions 로깅 // decisions 로깅
log.info("Minutes decisions 값 확인 - minutesId: {}, decisions: {}", log.info("Minutes decisions 값 확인 - minutesId: {}, decisions: {}",
minutes.getMinutesId(), minutes.getDecisions()); minutes.getMinutesId(), minutes.getDecisions());
@ -389,6 +399,7 @@ public class MinutesService implements
.todoCount(todoCount) .todoCount(todoCount)
.completedTodoCount(completedTodoCount) .completedTodoCount(completedTodoCount)
.participantCount(participantCount) .participantCount(participantCount)
.verificationRate(verificationRate)
.memo("") // 메모 필드는 추후 구현 .memo("") // 메모 필드는 추후 구현
.sections(sectionDTOs) // 섹션 정보 추가 .sections(sectionDTOs) // 섹션 정보 추가
.decisions(minutes.getDecisions()) // decisions 필드 추가 .decisions(minutes.getDecisions()) // decisions 필드 추가

View File

@ -56,4 +56,13 @@ public interface MinutesReader {
* @return AI 통합 회의록 * @return AI 통합 회의록
*/ */
Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId); Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId);
/**
* 사용자가 참여한 회의의 회의록 목록 조회
* 사용자가 생성했거나 참여한 회의의 회의록을 모두 조회
*
* @param userId 사용자 ID
* @return 회의록 목록
*/
List<Minutes> findByParticipantUserId(String userId);
} }

View File

@ -110,12 +110,13 @@ public class MinutesController {
// DTO를 Response 형식으로 변환 // DTO를 Response 형식으로 변환
List<MinutesListResponse.MinutesItem> minutesList = minutesPage.getContent().stream() List<MinutesListResponse.MinutesItem> minutesList = minutesPage.getContent().stream()
.map(this::convertToMinutesItem) .map(dto -> convertToMinutesItem(dto, userId))
.collect(Collectors.toList()); .collect(Collectors.toList());
// 필터링 적용 (상태별) // 필터링 적용 (상태별, 참여 유형별, 검색어)
List<MinutesListResponse.MinutesItem> filteredMinutes = minutesList.stream() List<MinutesListResponse.MinutesItem> filteredMinutes = minutesList.stream()
.filter(item -> filterByStatus(item, status)) .filter(item -> filterByStatus(item, status))
.filter(item -> filterByParticipationType(item, participationType))
.filter(item -> filterBySearch(item, search)) .filter(item -> filterBySearch(item, search))
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -489,8 +490,21 @@ public class MinutesController {
*/ */
private MinutesListResponse.Statistics calculateRealStatistics(String userId, String participationType) { private MinutesListResponse.Statistics calculateRealStatistics(String userId, String participationType) {
try { try {
// 전체 회의록 조회 (작성자 기준) // 전체 회의록 조회 (참여자 기준)
List<Minutes> allMinutes = minutesService.getMinutesByCreator(userId); List<MinutesDTO> allMinutes = minutesService.getMinutesListByUserId(userId, PageRequest.of(0, Integer.MAX_VALUE)).getContent();
// 참여 유형 필터링
if (participationType != null && !participationType.isEmpty()) {
if ("created".equals(participationType)) {
allMinutes = allMinutes.stream()
.filter(m -> userId.equals(m.getCreatedBy()))
.collect(Collectors.toList());
} else if ("attended".equals(participationType)) {
allMinutes = allMinutes.stream()
.filter(m -> !userId.equals(m.getCreatedBy()))
.collect(Collectors.toList());
}
}
long totalCount = allMinutes.size(); long totalCount = allMinutes.size();
long draftCount = allMinutes.stream() long draftCount = allMinutes.stream()
@ -515,10 +529,17 @@ public class MinutesController {
} }
} }
private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO) { private MinutesListResponse.MinutesItem convertToMinutesItem(MinutesDTO minutesDTO, String userId) {
// 완료율 계산 // 검증완료율 계산 (작성중 상태일 때는 verificationRate 사용, 확정완료시 100%)
int completionRate = minutesDTO.getTodoCount() > 0 ? int completionRate;
(minutesDTO.getCompletedTodoCount() * 100) / minutesDTO.getTodoCount() : 100; if ("DRAFT".equals(minutesDTO.getStatus()) && minutesDTO.getVerificationRate() != null) {
completionRate = minutesDTO.getVerificationRate();
} else if ("FINALIZED".equals(minutesDTO.getStatus())) {
completionRate = 100;
} else {
// 기본값 0
completionRate = 0;
}
return MinutesListResponse.MinutesItem.builder() return MinutesListResponse.MinutesItem.builder()
.minutesId(minutesDTO.getMinutesId()) .minutesId(minutesDTO.getMinutesId())
@ -535,7 +556,7 @@ public class MinutesController {
.todoCount(minutesDTO.getTodoCount()) .todoCount(minutesDTO.getTodoCount())
.completedTodoCount(minutesDTO.getCompletedTodoCount()) .completedTodoCount(minutesDTO.getCompletedTodoCount())
.completionRate(completionRate) .completionRate(completionRate)
.isCreatedByUser(true) // 현재는 작성자 기준으로만 조회하므로 true .isCreatedByUser(minutesDTO.getCreatedBy().equals(userId)) // 현재 사용자가 생성자인지 확인
.build(); .build();
} }
@ -556,10 +577,19 @@ public class MinutesController {
} }
/** /**
* 참여 유형별 필터링 - 현재는 사용하지 않음 (작성자 기준으로만 조회) * 참여 유형별 필터링
*/ */
private boolean filterByParticipationType(MinutesListResponse.MinutesItem item, String participationType, String userId) { private boolean filterByParticipationType(MinutesListResponse.MinutesItem item, String participationType) {
// 현재는 작성자 기준으로만 조회하므로 항상 true 반환 if (participationType == null || participationType.isEmpty()) {
return true; // 필터 미적용시 모두 표시
}
if ("created".equals(participationType)) {
return item.isCreatedByUser(); // 사용자가 생성한 회의록만
} else if ("attended".equals(participationType)) {
return !item.isCreatedByUser(); // 사용자가 참여만 회의록
}
return true; return true;
} }
@ -607,7 +637,7 @@ public class MinutesController {
private MinutesListResponse.Statistics calculateStatistics(List<MinutesListResponse.MinutesItem> allItems, private MinutesListResponse.Statistics calculateStatistics(List<MinutesListResponse.MinutesItem> allItems,
String participationType, String userId) { String participationType, String userId) {
List<MinutesListResponse.MinutesItem> filteredItems = allItems.stream() List<MinutesListResponse.MinutesItem> filteredItems = allItems.stream()
.filter(item -> filterByParticipationType(item, participationType, userId)) .filter(item -> filterByParticipationType(item, participationType))
.collect(Collectors.toList()); .collect(Collectors.toList());
long totalCount = filteredItems.size(); long totalCount = filteredItems.size();

View File

@ -18,8 +18,10 @@ import org.springframework.stereotype.Component;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -44,7 +46,7 @@ public class DashboardGateway implements DashboardReader {
// 1. 다가오는 회의 목록 조회 (향후 30일, 최대 10개) // 1. 다가오는 회의 목록 조회 (향후 30일, 최대 10개)
List<Meeting> upcomingMeetings = getUpcomingMeetings(userId); List<Meeting> upcomingMeetings = getUpcomingMeetings(userId);
// 2. 최근 회의록 목록 조회 (최근 7일, 최대 10) // 2. 최근 회의록 목록 조회 (최근 30일, 최대 4)
List<Minutes> recentMinutes = getRecentMinutes(userId); List<Minutes> recentMinutes = getRecentMinutes(userId);
// 3. 통계 정보 계산 (최근 30일 기준) // 3. 통계 정보 계산 (최근 30일 기준)
@ -235,36 +237,27 @@ public class DashboardGateway implements DashboardReader {
* 기간별 최근 회의록 목록 조회 * 기간별 최근 회의록 목록 조회
*/ */
private List<Minutes> getRecentMinutesByPeriod(String userId, LocalDateTime startTime, LocalDateTime endTime) { private List<Minutes> getRecentMinutesByPeriod(String userId, LocalDateTime startTime, LocalDateTime endTime) {
Set<String> userMinutesIds = new HashSet<>(); log.debug("회의록 조회 시작 - userId: {}, startTime: {}, endTime: {}", userId, startTime, endTime);
// 작성자로 참여한 회의록 조회 // 사용자가 작성한 회의록만 조회 (createdBy = userId)
List<MinutesEntity> createdMinutes = minutesJpaRepository.findByCreatedBy(userId).stream() List<MinutesEntity> userMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
.filter(m -> m.getCreatedAt().isAfter(startTime) && m.getCreatedAt().isBefore(endTime)) .filter(m -> m.getCreatedAt() != null &&
.toList(); m.getCreatedAt().isAfter(startTime) &&
m.getCreatedAt().isBefore(endTime))
createdMinutes.forEach(m -> userMinutesIds.add(m.getMinutesId()));
// 참석한 회의의 회의록 조회
List<String> participantMeetingIds = meetingParticipantJpaRepository.findByUserId(userId).stream()
.map(p -> p.getMeetingId())
.toList();
List<MinutesEntity> participatedMinutes = minutesJpaRepository.findAll().stream()
.filter(m -> participantMeetingIds.contains(m.getMeetingId()))
.filter(m -> m.getCreatedAt().isAfter(startTime) && m.getCreatedAt().isBefore(endTime))
.toList();
participatedMinutes.forEach(m -> userMinutesIds.add(m.getMinutesId()));
// 중복 제거 최종 수정 시간순 정렬
return minutesJpaRepository.findAll().stream()
.filter(m -> userMinutesIds.contains(m.getMinutesId()))
.sorted((m1, m2) -> { .sorted((m1, m2) -> {
LocalDateTime time1 = m1.getUpdatedAt() != null ? m1.getUpdatedAt() : m1.getCreatedAt(); LocalDateTime time1 = m1.getUpdatedAt() != null ? m1.getUpdatedAt() : m1.getCreatedAt();
LocalDateTime time2 = m2.getUpdatedAt() != null ? m2.getUpdatedAt() : m2.getCreatedAt(); LocalDateTime time2 = m2.getUpdatedAt() != null ? m2.getUpdatedAt() : m2.getCreatedAt();
return time2.compareTo(time1); // 최신순 return time2.compareTo(time1); // 최신순
}) })
// limit 제거 - 상위 메서드에서 필터링 .toList();
log.debug("조회된 회의록 수: {}", userMinutes.size());
userMinutes.forEach(m -> {
log.debug(" - minutesId: {}, meetingId: {}, title: {}, createdBy: {}, createdAt: {}",
m.getMinutesId(), m.getMeetingId(), m.getTitle(), m.getCreatedBy(), m.getCreatedAt());
});
return userMinutes.stream()
.map(MinutesEntity::toDomain) .map(MinutesEntity::toDomain)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }

View File

@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -75,9 +76,32 @@ public class MinutesGateway implements MinutesReader, MinutesWriter {
@Override @Override
public Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId) { public Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId) {
log.debug("회의 ID로 AI 통합 회의록 조회: {}", meetingId); log.debug("회의 ID로 AI 통합 회의록 조회: {}", meetingId);
return minutesJpaRepository.findByMeetingIdAndUserIdIsNull(meetingId) List<MinutesEntity> consolidatedMinutes = minutesJpaRepository.findByMeetingIdAndUserIdIsNull(meetingId);
if (consolidatedMinutes.isEmpty()) {
return Optional.empty();
}
// 여러 개가 있을 경우 가장 최신 것을 반환 (updatedAt 또는 createdAt 기준)
return consolidatedMinutes.stream()
.sorted((m1, m2) -> {
LocalDateTime time1 = m1.getUpdatedAt() != null ? m1.getUpdatedAt() : m1.getCreatedAt();
LocalDateTime time2 = m2.getUpdatedAt() != null ? m2.getUpdatedAt() : m2.getCreatedAt();
return time2.compareTo(time1); // 최신순
})
.findFirst()
.map(MinutesEntity::toDomain); .map(MinutesEntity::toDomain);
} }
@Override
public List<Minutes> findByParticipantUserId(String userId) {
log.debug("사용자가 참여한 회의의 회의록 조회: {}", userId);
// 사용자가 생성한 회의록과 사용자가 참여한 회의의 회의록을 모두 조회
// 현재는 생성자 기준으로만 조회 (추후 참석자 테이블과 조인하여 구현 필요)
return minutesJpaRepository.findByCreatedByOrParticipantUserId(userId).stream()
.map(MinutesEntity::toDomain)
.collect(Collectors.toList());
}
@Override @Override
public Minutes save(Minutes minutes) { public Minutes save(Minutes minutes) {

View File

@ -50,7 +50,20 @@ public interface MinutesJpaRepository extends JpaRepository<MinutesEntity, Strin
List<MinutesEntity> findByMeetingIdAndUserIdIsNotNull(String meetingId); List<MinutesEntity> findByMeetingIdAndUserIdIsNotNull(String meetingId);
/** /**
* 회의 ID로 AI 통합 회의록 조회 (user_id IS NULL) * 회의 ID로 AI 통합 회의록 목록 조회 (user_id IS NULL)
*/ */
Optional<MinutesEntity> findByMeetingIdAndUserIdIsNull(String meetingId); List<MinutesEntity> findByMeetingIdAndUserIdIsNull(String meetingId);
/**
* 사용자가 생성했거나 참여한 회의의 회의록 조회
* 현재는 생성자 기준으로만 조회 (추후 참석자 테이블과 조인 필요)
*
* @param userId 사용자 ID
* @return 회의록 목록
*/
default List<MinutesEntity> findByCreatedByOrParticipantUserId(String userId) {
// TODO: 참석자 테이블(participants) 조인하여 참여한 회의의 회의록도 조회하도록 구현 필요
// 현재는 임시로 생성자 기준으로만 조회
return findByCreatedBy(userId);
}
} }