회의록 목록 조회 API (mock) 구현

This commit is contained in:
cyjadela
2025-10-27 13:12:28 +09:00
parent a7ce5a6edd
commit 279bfa0758
25 changed files with 788 additions and 28 deletions
@@ -58,44 +58,51 @@ public class MinutesController {
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-User-Name") String userName,
@Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size,
@Parameter(description = "정렬 기준 (createdAt, lastModifiedAt)") @RequestParam(defaultValue = "lastModifiedAt") String sortBy,
@Parameter(description = "정렬 방향 (asc, desc)") @RequestParam(defaultValue = "desc") String sortDir) {
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "10") int size,
@Parameter(description = "정렬 기준 (modified, meeting, title)") @RequestParam(defaultValue = "modified") String sortBy,
@Parameter(description = "정렬 방향 (asc, desc)") @RequestParam(defaultValue = "desc") String sortDir,
@Parameter(description = "상태 필터 (all, draft, complete)") @RequestParam(defaultValue = "all") String status,
@Parameter(description = "참여 유형 (attended, created)") @RequestParam(required = false) String participationType,
@Parameter(description = "검색 키워드") @RequestParam(required = false) String search) {
log.info("회의록 목록 조회 요청 - userId: {}, page: {}, size: {}", userId, page, size);
log.info("회의록 목록 조회 요청 - userId: {}, page: {}, size: {}, status: {}, participationType: {}, search: {}",
userId, page, size, status, participationType, search);
try {
// 캐시 확인
String cacheKey = String.format("minutes:list:%s:%d:%d:%s:%s", userId, page, size, sortBy, sortDir);
MinutesListResponse cachedResponse = cacheService.getCachedMinutesList(cacheKey);
if (cachedResponse != null) {
log.debug("캐시된 회의록 목록 반환 - userId: {}", userId);
return ResponseEntity.ok(ApiResponse.success(cachedResponse));
}
// 정렬 설정
Sort.Direction direction = sortDir.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
// 회의록 목록 조회
var minutesPage = minutesService.getMinutesListByUserId(userId, pageable);
// Mock 데이터 생성 (프론트엔드 테스트용)
List<MinutesListResponse.MinutesItem> mockMinutes = createMockMinutesList(userId);
// 응답 DTO 생성
List<MinutesListResponse.MinutesItem> minutesItems = minutesPage.getContent().stream()
.map(this::convertToMinutesItem)
// 필터링 적용
List<MinutesListResponse.MinutesItem> filteredMinutes = mockMinutes.stream()
.filter(item -> filterByStatus(item, status))
.filter(item -> filterByParticipationType(item, participationType, userId))
.filter(item -> filterBySearch(item, search))
.collect(Collectors.toList());
// 정렬 적용
applySorting(filteredMinutes, sortBy, sortDir);
// 페이징 적용
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()
.minutesList(minutesItems)
.totalCount(minutesPage.getTotalElements())
.minutesList(pagedMinutes)
.totalCount(filteredMinutes.size())
.currentPage(page)
.totalPages(minutesPage.getTotalPages())
.totalPages((int) Math.ceil((double) filteredMinutes.size() / size))
.statistics(stats)
.build();
// 캐시 저장
cacheService.cacheMinutesList(cacheKey, response);
log.info("회의록 목록 조회 성공 - userId: {}, count: {}", userId, minutesItems.size());
log.info("회의록 목록 조회 성공 - userId: {}, total: {}, filtered: {}, paged: {}",
userId, mockMinutes.size(), filteredMinutes.size(), pagedMinutes.size());
return ResponseEntity.ok(ApiResponse.success(response));
} catch (Exception e) {
@@ -345,6 +352,238 @@ public class MinutesController {
.build();
}
/**
* Mock 데이터 생성 (프론트엔드 테스트용)
*/
private List<MinutesListResponse.MinutesItem> createMockMinutesList(String userId) {
List<MinutesListResponse.MinutesItem> mockData = List.of(
// 사용자가 생성한 회의록들
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-001")
.title("2024년 1분기 성과리뷰 회의록")
.meetingTitle("2024년 1분기 성과리뷰")
.status("FINALIZED")
.version(3)
.createdAt(LocalDateTime.of(2024, 3, 15, 14, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 15, 16, 30))
.meetingDate(LocalDateTime.of(2024, 3, 15, 14, 0))
.createdBy(userId)
.lastModifiedBy(userId)
.participantCount(8)
.todoCount(5)
.completedTodoCount(5)
.completionRate(100)
.isCreatedByUser(true)
.build(),
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-002")
.title("신규 프로젝트 킥오프 회의록")
.meetingTitle("신규 프로젝트 킥오프")
.status("DRAFT")
.version(1)
.createdAt(LocalDateTime.of(2024, 3, 20, 10, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 20, 11, 45))
.meetingDate(LocalDateTime.of(2024, 3, 20, 10, 0))
.createdBy(userId)
.lastModifiedBy("user-002")
.participantCount(6)
.todoCount(8)
.completedTodoCount(3)
.completionRate(75)
.isCreatedByUser(true)
.build(),
// 사용자가 참석한 회의록들
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-003")
.title("마케팅 전략 회의록")
.meetingTitle("마케팅 전략 논의")
.status("FINALIZED")
.version(2)
.createdAt(LocalDateTime.of(2024, 3, 18, 15, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 18, 17, 0))
.meetingDate(LocalDateTime.of(2024, 3, 18, 15, 0))
.createdBy("user-003")
.lastModifiedBy("user-003")
.participantCount(5)
.todoCount(4)
.completedTodoCount(4)
.completionRate(100)
.isCreatedByUser(false)
.build(),
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-004")
.title("기술 아키텍처 리뷰 회의록")
.meetingTitle("기술 아키텍처 리뷰")
.status("DRAFT")
.version(1)
.createdAt(LocalDateTime.of(2024, 3, 22, 9, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 22, 10, 30))
.meetingDate(LocalDateTime.of(2024, 3, 22, 9, 0))
.createdBy("user-004")
.lastModifiedBy("user-004")
.participantCount(7)
.todoCount(6)
.completedTodoCount(2)
.completionRate(60)
.isCreatedByUser(false)
.build(),
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-005")
.title("주간 스프린트 회고 회의록")
.meetingTitle("주간 스프린트 회고")
.status("FINALIZED")
.version(1)
.createdAt(LocalDateTime.of(2024, 3, 25, 16, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 25, 17, 0))
.meetingDate(LocalDateTime.of(2024, 3, 25, 16, 0))
.createdBy("user-005")
.lastModifiedBy("user-005")
.participantCount(4)
.todoCount(3)
.completedTodoCount(3)
.completionRate(100)
.isCreatedByUser(false)
.build(),
// 추가 더미 데이터들
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-006")
.title("고객 피드백 분석 회의록")
.meetingTitle("고객 피드백 분석")
.status("DRAFT")
.version(2)
.createdAt(LocalDateTime.of(2024, 3, 28, 14, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 28, 15, 20))
.meetingDate(LocalDateTime.of(2024, 3, 28, 14, 0))
.createdBy(userId)
.lastModifiedBy(userId)
.participantCount(5)
.todoCount(7)
.completedTodoCount(4)
.completionRate(85)
.isCreatedByUser(true)
.build(),
MinutesListResponse.MinutesItem.builder()
.minutesId("minutes-007")
.title("보안 정책 수립 회의록")
.meetingTitle("보안 정책 수립")
.status("FINALIZED")
.version(1)
.createdAt(LocalDateTime.of(2024, 3, 12, 10, 0))
.lastModifiedAt(LocalDateTime.of(2024, 3, 12, 12, 0))
.meetingDate(LocalDateTime.of(2024, 3, 12, 10, 0))
.createdBy("user-006")
.lastModifiedBy("user-006")
.participantCount(6)
.todoCount(4)
.completedTodoCount(4)
.completionRate(100)
.isCreatedByUser(false)
.build()
);
return mockData;
}
/**
* 상태별 필터링
*/
private boolean filterByStatus(MinutesListResponse.MinutesItem item, String status) {
if ("all".equals(status)) {
return true;
}
if ("draft".equals(status)) {
return "DRAFT".equals(item.getStatus());
}
if ("complete".equals(status)) {
return "FINALIZED".equals(item.getStatus());
}
return true;
}
/**
* 참여 유형별 필터링
*/
private boolean filterByParticipationType(MinutesListResponse.MinutesItem item, String participationType, String userId) {
if (participationType == null || participationType.isEmpty()) {
return true;
}
if ("created".equals(participationType)) {
return item.isCreatedByUser();
}
if ("attended".equals(participationType)) {
return !item.isCreatedByUser();
}
return true;
}
/**
* 검색어 필터링
*/
private boolean filterBySearch(MinutesListResponse.MinutesItem item, String search) {
if (search == null || search.trim().isEmpty()) {
return true;
}
String searchLower = search.toLowerCase();
return item.getTitle().toLowerCase().contains(searchLower) ||
item.getMeetingTitle().toLowerCase().contains(searchLower);
}
/**
* 정렬 적용
*/
private void applySorting(List<MinutesListResponse.MinutesItem> items, String sortBy, String sortDir) {
boolean ascending = "asc".equalsIgnoreCase(sortDir);
switch (sortBy) {
case "title":
items.sort((a, b) -> ascending ?
a.getTitle().compareTo(b.getTitle()) :
b.getTitle().compareTo(a.getTitle()));
break;
case "meeting":
items.sort((a, b) -> ascending ?
a.getMeetingDate().compareTo(b.getMeetingDate()) :
b.getMeetingDate().compareTo(a.getMeetingDate()));
break;
case "modified":
default:
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, userId))
.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();
}
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
return MinutesDetailResponse.builder()
.minutesId(minutesDTO.getMinutesId())
@@ -21,6 +21,17 @@ public class MinutesListResponse {
private long totalCount;
private int currentPage;
private int totalPages;
private Statistics statistics; // 상태별 통계
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Statistics {
private long totalCount;
private long draftCount;
private long completeCount;
}
@Getter
@Builder
@@ -34,9 +45,13 @@ public class MinutesListResponse {
private int version;
private LocalDateTime createdAt;
private LocalDateTime lastModifiedAt;
private LocalDateTime meetingDate; // 회의 일시
private String createdBy;
private String lastModifiedBy;
private int participantCount; // 참석자 수
private int todoCount;
private int completedTodoCount;
private int completionRate; // 검증완료율
private boolean isCreatedByUser; // 사용자가 생성한 회의록 여부
}
}