mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 05:59:11 +00:00
Merge branch 'main' of https://github.com/hwanny1128/HGZero into chore/path
This commit is contained in:
@@ -18,12 +18,14 @@ import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.cache.CacheService;
|
||||
import com.unicorn.hgzero.meeting.infra.event.dto.MeetingStartedEvent;
|
||||
import com.unicorn.hgzero.meeting.infra.event.dto.NotificationRequestEvent;
|
||||
import com.unicorn.hgzero.meeting.infra.event.publisher.EventPublisher;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -326,9 +328,12 @@ public class MeetingService implements
|
||||
log.debug("Found minutes: {}", minutes.getTitle());
|
||||
}
|
||||
|
||||
// 5. AI 분석 수행 (현재는 Mock 데이터로 구현)
|
||||
MeetingAnalysis analysis = performAIAnalysis(meeting, minutes);
|
||||
// 5. 기본 분석 정보 생성 (실제 AI 분석은 AI 서비스에서 비동기 처리)
|
||||
MeetingAnalysis analysis = createBasicAnalysis(meeting, minutes);
|
||||
meetingAnalysisWriter.save(analysis);
|
||||
|
||||
// TODO: AI 서비스에 비동기 분석 요청 전송
|
||||
// aiAnalysisClient.requestAnalysis(meeting.getMeetingId(), minutes.getMinutesId());
|
||||
|
||||
// 6. 결과 DTO 구성
|
||||
MeetingEndDTO result = buildMeetingEndDTO(meeting, analysis);
|
||||
@@ -338,55 +343,20 @@ public class MeetingService implements
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 수행 (Mock 구현)
|
||||
* 간단한 회의 분석 수행 (AI 서비스 없이 기본 정보만 처리)
|
||||
* AI 분석은 별도 AI 서비스에서 비동기로 처리되어야 함
|
||||
*/
|
||||
private MeetingAnalysis performAIAnalysis(Meeting meeting, Minutes minutes) {
|
||||
log.info("Performing AI analysis for meeting: {}", meeting.getMeetingId());
|
||||
|
||||
// Mock 데이터로 구현 (실제로는 AI 서비스 호출)
|
||||
List<String> keywords = List.of(
|
||||
"#신제품기획", "#예산편성", "#일정조율",
|
||||
"#시장조사", "#UI/UX", "#개발스펙"
|
||||
);
|
||||
|
||||
List<MeetingAnalysis.AgendaAnalysis> agendaAnalyses = List.of(
|
||||
MeetingAnalysis.AgendaAnalysis.builder()
|
||||
.agendaId("agenda-1")
|
||||
.title("1. 신제품 기획 방향성")
|
||||
.aiSummaryShort("타겟 고객을 20-30대로 설정, UI/UX 개선 집중")
|
||||
.discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고, 기존 제품 대비 UI/UX를 대폭 개선하기로 함")
|
||||
.decisions(List.of("타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정"))
|
||||
.pending(List.of())
|
||||
.extractedTodos(List.of("시장 조사 보고서 작성", "UI/UX 개선안 초안 작성"))
|
||||
.build(),
|
||||
MeetingAnalysis.AgendaAnalysis.builder()
|
||||
.agendaId("agenda-2")
|
||||
.title("2. 예산 편성 및 일정")
|
||||
.aiSummaryShort("총 예산 5억, 개발 기간 6개월 확정")
|
||||
.discussion("신제품 개발을 위한 총 예산을 5억원으로 책정하고, 개발 기간은 6개월로 확정함")
|
||||
.decisions(List.of("총 예산: 5억원", "개발 기간: 6개월", "예산 배분: 개발 60%, 마케팅 40%"))
|
||||
.pending(List.of("세부 일정 확정은 다음 회의에서 논의"))
|
||||
.extractedTodos(List.of("세부 개발 일정 수립"))
|
||||
.build(),
|
||||
MeetingAnalysis.AgendaAnalysis.builder()
|
||||
.agendaId("agenda-3")
|
||||
.title("3. 기술 스택 및 개발 방향")
|
||||
.aiSummaryShort("React 기반 프론트엔드, AI 챗봇 기능 추가")
|
||||
.discussion("프론트엔드는 React 기반으로 개발하고, 고객 지원을 위한 AI 챗봇 기능을 추가하기로 함")
|
||||
.decisions(List.of("프론트엔드: React 기반", "AI 챗봇 기능 추가", "Next.js 도입 검토"))
|
||||
.pending(List.of("AI 챗봇 학습 데이터 확보 방안"))
|
||||
.extractedTodos(List.of("AI 챗봇 프로토타입 개발", "Next.js 도입 검토 보고서"))
|
||||
.build()
|
||||
);
|
||||
private MeetingAnalysis createBasicAnalysis(Meeting meeting, Minutes minutes) {
|
||||
log.info("Creating basic analysis for meeting: {}", meeting.getMeetingId());
|
||||
|
||||
// 기본 분석 정보만 생성 (키워드나 상세 분석은 AI 서비스에서 별도 처리)
|
||||
return MeetingAnalysis.builder()
|
||||
.analysisId(UUID.randomUUID().toString())
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.keywords(keywords)
|
||||
.agendaAnalyses(agendaAnalyses)
|
||||
.status("COMPLETED")
|
||||
.completedAt(LocalDateTime.now())
|
||||
.keywords(List.of()) // AI 서비스에서 별도로 채워짐
|
||||
.agendaAnalyses(List.of()) // AI 서비스에서 별도로 채워짐
|
||||
.status("PENDING") // AI 처리 대기 상태
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
@@ -395,10 +365,11 @@ public class MeetingService implements
|
||||
* MeetingEndDTO 구성
|
||||
*/
|
||||
private MeetingEndDTO buildMeetingEndDTO(Meeting meeting, MeetingAnalysis analysis) {
|
||||
// 회의 시간 계산 (Mock)
|
||||
int durationMinutes = 90;
|
||||
// 회의 시간 및 참석자 수 계산 (실제 데이터 기반)
|
||||
int durationMinutes = calculateActualDuration(meeting);
|
||||
int participantCount = calculateActualParticipantCount(meeting.getMeetingId());
|
||||
|
||||
// 전체 Todo 개수 계산
|
||||
// 전체 Todo 개수 계산 (AI 분석 완료되기 전까지는 0)
|
||||
int totalTodos = analysis.getAgendaAnalyses().stream()
|
||||
.mapToInt(agenda -> agenda.getExtractedTodos().size())
|
||||
.sum();
|
||||
@@ -423,7 +394,7 @@ public class MeetingService implements
|
||||
|
||||
return MeetingEndDTO.builder()
|
||||
.title(meeting.getTitle())
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.participantCount(participantCount)
|
||||
.durationMinutes(durationMinutes)
|
||||
.agendaCount(analysis.getAgendaAnalyses().size())
|
||||
.todoCount(totalTodos)
|
||||
@@ -459,6 +430,36 @@ public class MeetingService implements
|
||||
return updatedMeeting;
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 실제 진행 시간 계산
|
||||
*/
|
||||
private int calculateActualDuration(Meeting meeting) {
|
||||
if (meeting.getStartedAt() != null && meeting.getEndedAt() != null) {
|
||||
// 실제 시작/종료 시간이 있으면 실제 진행 시간 계산
|
||||
Duration duration = Duration.between(meeting.getStartedAt(), meeting.getEndedAt());
|
||||
return (int) duration.toMinutes();
|
||||
} else if (meeting.getScheduledAt() != null && meeting.getEndTime() != null) {
|
||||
// 예정 시간 기준으로 계산
|
||||
Duration duration = Duration.between(meeting.getScheduledAt(), meeting.getEndTime());
|
||||
return (int) duration.toMinutes();
|
||||
} else {
|
||||
// 기본값 (1시간)
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 참석자 수 계산
|
||||
*/
|
||||
private int calculateActualParticipantCount(String meetingId) {
|
||||
// 실제 참석한 참석자 수 계산 (meeting_participants 테이블에서 attended=true인 수)
|
||||
// 현재는 기본적으로 주최자 + 참석자 수로 계산
|
||||
List<String> participants = meetingReader.findById(meetingId)
|
||||
.map(Meeting::getParticipants)
|
||||
.orElse(List.of());
|
||||
return participants.size() + 1; // 주최자 포함
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 회의 조회
|
||||
*/
|
||||
@@ -541,12 +542,31 @@ public class MeetingService implements
|
||||
// 참석자 저장
|
||||
participantWriter.saveParticipant(command.meetingId(), command.email());
|
||||
|
||||
// TODO: 실제 이메일 발송 구현 필요
|
||||
// 이메일 발송 서비스 호출
|
||||
// emailService.sendInvitation(command.email(), meeting, command.frontendUrl());
|
||||
// 현재는 로그만 남기고 성공으로 처리
|
||||
log.info("Invitation email would be sent to {} for meeting {} (Frontend URL: {})",
|
||||
command.email(), meeting.getTitle(), command.frontendUrl());
|
||||
// 회의 초대 알림 이벤트 발행
|
||||
try {
|
||||
NotificationRequestEvent event = NotificationRequestEvent.builder()
|
||||
.notificationType("MEETING_INVITATION")
|
||||
.recipientEmail(command.email())
|
||||
.recipientId(command.email())
|
||||
.recipientName(command.email())
|
||||
.title("회의 초대")
|
||||
.message(String.format("'%s' 회의에 초대되었습니다. 일시: %s, 장소: %s, 참여 링크: %s",
|
||||
meeting.getTitle(), meeting.getScheduledAt(), meeting.getLocation(), command.frontendUrl()))
|
||||
.relatedEntityId(command.meetingId())
|
||||
.relatedEntityType("MEETING")
|
||||
.requestedBy(meeting.getOrganizerId())
|
||||
.requestedByName(command.inviterName())
|
||||
.eventTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
eventPublisher.publishNotificationRequest(event);
|
||||
log.info("Meeting invitation event published for email: {}, meetingId: {}",
|
||||
command.email(), command.meetingId());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to publish meeting invitation event: meetingId={}, email={}",
|
||||
command.meetingId(), command.email(), e);
|
||||
// 이벤트 발행 실패는 비즈니스 로직에 영향을 주지 않으므로 계속 진행
|
||||
}
|
||||
|
||||
log.info("Participant invited successfully: {} to meeting {}", command.email(), command.meetingId());
|
||||
}
|
||||
|
||||
+60
-74
@@ -1,7 +1,10 @@
|
||||
package com.unicorn.hgzero.meeting.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.dashboard.GetDashboardUseCase;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.DashboardResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.mapper.DashboardResponseMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
@@ -9,7 +12,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -28,8 +30,11 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class DashboardController {
|
||||
|
||||
private final GetDashboardUseCase getDashboardUseCase;
|
||||
private final DashboardResponseMapper dashboardResponseMapper;
|
||||
|
||||
/**
|
||||
* 대시보드 데이터 조회 (목 데이터)
|
||||
* 대시보드 데이터 조회
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @return 대시보드 데이터
|
||||
@@ -50,80 +55,61 @@ public class DashboardController {
|
||||
|
||||
log.info("대시보드 데이터 조회 요청 - userId: {}", userId);
|
||||
|
||||
// 목 데이터 생성
|
||||
DashboardResponse mockResponse = createMockDashboardData();
|
||||
try {
|
||||
// 실제 데이터 조회
|
||||
Dashboard dashboard = getDashboardUseCase.getDashboard(userId);
|
||||
|
||||
// 도메인 객체를 응답 DTO로 변환
|
||||
DashboardResponse response = dashboardResponseMapper.toResponse(dashboard);
|
||||
|
||||
log.info("대시보드 데이터 조회 완료 - userId: {}", userId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
} catch (Exception e) {
|
||||
log.error("대시보드 데이터 조회 실패 - userId: {}", userId, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 대시보드 데이터 조회
|
||||
*
|
||||
* @param userId 사용자 ID
|
||||
* @param period 조회 기간 (1day, 3days, 7days, 30days, 90days)
|
||||
* @return 기간별 대시보드 데이터
|
||||
*/
|
||||
@Operation(
|
||||
summary = "기간별 대시보드 데이터 조회",
|
||||
description = "사용자별 맞춤 대시보드 정보를 기간 필터로 조회합니다.",
|
||||
security = @SecurityRequirement(name = "bearerAuth")
|
||||
)
|
||||
@GetMapping("/period/{period}")
|
||||
public ResponseEntity<ApiResponse<DashboardResponse>> getDashboardByPeriod(
|
||||
@Parameter(description = "사용자 ID", required = true)
|
||||
@RequestHeader("X-User-Id") String userId,
|
||||
@Parameter(description = "사용자명", required = true)
|
||||
@RequestHeader("X-User-Name") String userName,
|
||||
@Parameter(description = "사용자 이메일", required = true)
|
||||
@RequestHeader("X-User-Email") String userEmail,
|
||||
@Parameter(description = "조회 기간", required = true)
|
||||
@PathVariable String period) {
|
||||
|
||||
log.info("대시보드 데이터 조회 완료 - userId: {}", userId);
|
||||
log.info("기간별 대시보드 데이터 조회 요청 - userId: {}, period: {}", userId, period);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(mockResponse));
|
||||
try {
|
||||
// 실제 데이터 조회
|
||||
Dashboard dashboard = getDashboardUseCase.getDashboardByPeriod(userId, period);
|
||||
|
||||
// 도메인 객체를 응답 DTO로 변환
|
||||
DashboardResponse response = dashboardResponseMapper.toResponse(dashboard);
|
||||
|
||||
log.info("기간별 대시보드 데이터 조회 완료 - userId: {}, period: {}", userId, period);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
} catch (Exception e) {
|
||||
log.error("기간별 대시보드 데이터 조회 실패 - userId: {}, period: {}", userId, period, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 목 데이터 생성
|
||||
*/
|
||||
private DashboardResponse createMockDashboardData() {
|
||||
// 예정된 회의 목 데이터
|
||||
List<DashboardResponse.UpcomingMeetingResponse> upcomingMeetings = Arrays.asList(
|
||||
DashboardResponse.UpcomingMeetingResponse.builder()
|
||||
.meetingId("550e8400-e29b-41d4-a716-446655440001")
|
||||
.title("Q1 전략 회의")
|
||||
.startTime(LocalDateTime.now().plusDays(2).withHour(14).withMinute(0))
|
||||
.endTime(LocalDateTime.now().plusDays(2).withHour(16).withMinute(0))
|
||||
.location("회의실 A")
|
||||
.participantCount(5)
|
||||
.status("SCHEDULED")
|
||||
.build(),
|
||||
DashboardResponse.UpcomingMeetingResponse.builder()
|
||||
.meetingId("550e8400-e29b-41d4-a716-446655440002")
|
||||
.title("개발팀 스프린트 계획")
|
||||
.startTime(LocalDateTime.now().plusDays(3).withHour(10).withMinute(0))
|
||||
.endTime(LocalDateTime.now().plusDays(3).withHour(12).withMinute(0))
|
||||
.location("회의실 B")
|
||||
.participantCount(8)
|
||||
.status("SCHEDULED")
|
||||
.build()
|
||||
);
|
||||
|
||||
// 최근 회의록 목 데이터
|
||||
List<DashboardResponse.RecentMinutesResponse> recentMinutes = Arrays.asList(
|
||||
DashboardResponse.RecentMinutesResponse.builder()
|
||||
.minutesId("770e8400-e29b-41d4-a716-446655440001")
|
||||
.title("아키텍처 설계 회의")
|
||||
.meetingDate(LocalDateTime.now().minusDays(1).withHour(14).withMinute(0))
|
||||
.status("FINALIZED")
|
||||
.participantCount(6)
|
||||
.lastModified(LocalDateTime.now().minusDays(1).withHour(16).withMinute(30))
|
||||
.build(),
|
||||
DashboardResponse.RecentMinutesResponse.builder()
|
||||
.minutesId("770e8400-e29b-41d4-a716-446655440002")
|
||||
.title("UI/UX 검토 회의")
|
||||
.meetingDate(LocalDateTime.now().minusDays(3).withHour(11).withMinute(0))
|
||||
.status("FINALIZED")
|
||||
.participantCount(4)
|
||||
.lastModified(LocalDateTime.now().minusDays(3).withHour(12).withMinute(45))
|
||||
.build(),
|
||||
DashboardResponse.RecentMinutesResponse.builder()
|
||||
.minutesId("770e8400-e29b-41d4-a716-446655440003")
|
||||
.title("API 설계 검토")
|
||||
.meetingDate(LocalDateTime.now().minusDays(5).withHour(15).withMinute(0))
|
||||
.status("DRAFT")
|
||||
.participantCount(3)
|
||||
.lastModified(LocalDateTime.now().minusDays(5).withHour(16).withMinute(15))
|
||||
.build()
|
||||
);
|
||||
|
||||
// 통계 정보 목 데이터
|
||||
DashboardResponse.StatisticsResponse statistics = DashboardResponse.StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(2)
|
||||
.activeTodosCount(0) // activeTodos 제거로 0으로 설정
|
||||
.todoCompletionRate(0.0) // activeTodos 제거로 0으로 설정
|
||||
.build();
|
||||
|
||||
return DashboardResponse.builder()
|
||||
.upcomingMeetings(upcomingMeetings)
|
||||
.activeTodos(Collections.emptyList()) // activeTodos 빈 리스트로 설정
|
||||
.myMinutes(recentMinutes)
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+20
-93
@@ -1,7 +1,10 @@
|
||||
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.common.exception.ErrorCode;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.CreateMeetingRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.InviteParticipantRequest;
|
||||
@@ -191,101 +194,25 @@ public class MeetingController {
|
||||
|
||||
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
// Mock 데이터로 응답 (개발용)
|
||||
var response = createMockMeetingEndResponse(meetingId);
|
||||
|
||||
log.info("회의 종료 완료 (Mock) - meetingId: {}", meetingId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
try {
|
||||
// 실제 비즈니스 로직 호출
|
||||
MeetingEndDTO meetingEndDTO = endMeetingUseCase.endMeeting(meetingId);
|
||||
|
||||
// DTO를 응답 객체로 변환
|
||||
MeetingEndResponse response = MeetingEndResponse.from(meetingEndDTO);
|
||||
|
||||
log.info("회의 종료 완료 - meetingId: {}", meetingId);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (BusinessException e) {
|
||||
log.error("회의 종료 실패 - meetingId: {}, error: {}", meetingId, e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("회의 종료 중 예상치 못한 오류 - meetingId: {}", meetingId, e);
|
||||
throw new BusinessException(ErrorCode.INTERNAL_SERVER_ERROR, "회의 종료 처리 중 오류가 발생했습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 종료 응답 Mock 데이터 생성
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return Mock 회의 종료 응답
|
||||
*/
|
||||
private MeetingEndResponse createMockMeetingEndResponse(String meetingId) {
|
||||
return MeetingEndResponse.builder()
|
||||
.title("Q1 전략 기획 회의")
|
||||
.participantCount(4)
|
||||
.durationMinutes(90)
|
||||
.agendaCount(3)
|
||||
.todoCount(5)
|
||||
.keywords(List.of("신제품 기획", "마케팅 전략", "예산 계획", "UI/UX 개선", "고객 분석"))
|
||||
.agendaSummaries(List.of(
|
||||
MeetingEndResponse.AgendaSummary.builder()
|
||||
.title("1. 신제품 기획 방향성")
|
||||
.aiSummaryShort("타겟 고객을 20-30대로 설정하고 UI/UX 개선에 집중하기로 결정")
|
||||
.details(MeetingEndResponse.AgendaDetails.builder()
|
||||
.discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고 모바일 중심의 사용자 경험을 강화하는 방향으로 논의됨")
|
||||
.decisions(List.of(
|
||||
"타겟 고객: 20-30대 직장인",
|
||||
"플랫폼: 모바일 우선",
|
||||
"핵심 기능: 간편 결제, 개인화 추천"
|
||||
))
|
||||
.pending(List.of(
|
||||
"경쟁사 분석 보완 필요",
|
||||
"기술 스택 최종 검토"
|
||||
))
|
||||
.build())
|
||||
.todos(List.of(
|
||||
MeetingEndResponse.TodoSummary.builder()
|
||||
.title("시장 조사 보고서 작성")
|
||||
.build(),
|
||||
MeetingEndResponse.TodoSummary.builder()
|
||||
.title("와이어프레임 초안 제작")
|
||||
.build()
|
||||
))
|
||||
.build(),
|
||||
MeetingEndResponse.AgendaSummary.builder()
|
||||
.title("2. 마케팅 전략 수립")
|
||||
.aiSummaryShort("SNS 마케팅과 인플루언서 협업을 통한 브랜드 인지도 향상 계획")
|
||||
.details(MeetingEndResponse.AgendaDetails.builder()
|
||||
.discussion("초기 론칭 시 SNS 중심의 마케팅 전략과 마이크로 인플루언서 협업을 통한 브랜드 인지도 향상 방안 논의")
|
||||
.decisions(List.of(
|
||||
"마케팅 채널: 인스타그램, 틱톡 우선",
|
||||
"예산 배분: 인플루언서 50%, 광고 30%, 이벤트 20%",
|
||||
"론칭 시기: 2024년 2분기"
|
||||
))
|
||||
.pending(List.of(
|
||||
"인플루언서 리스트 검토",
|
||||
"마케팅 예산 최종 승인"
|
||||
))
|
||||
.build())
|
||||
.todos(List.of(
|
||||
MeetingEndResponse.TodoSummary.builder()
|
||||
.title("인플루언서 후보 리스트 작성")
|
||||
.build(),
|
||||
MeetingEndResponse.TodoSummary.builder()
|
||||
.title("마케팅 예산안 상세 작성")
|
||||
.build()
|
||||
))
|
||||
.build(),
|
||||
MeetingEndResponse.AgendaSummary.builder()
|
||||
.title("3. 프로젝트 일정 및 리소스")
|
||||
.aiSummaryShort("개발 6개월, 테스트 2개월로 총 8개월 일정 확정")
|
||||
.details(MeetingEndResponse.AgendaDetails.builder()
|
||||
.discussion("전체 프로젝트 일정을 8개월로 설정하고 개발팀 6명, 디자인팀 2명으로 팀 구성 확정")
|
||||
.decisions(List.of(
|
||||
"전체 일정: 8개월 (개발 6개월, 테스트 2개월)",
|
||||
"팀 구성: 개발 6명, 디자인 2명, PM 1명",
|
||||
"주요 마일스톤: MVP 3개월, 베타 6개월, 정식 출시 8개월"
|
||||
))
|
||||
.pending(List.of(
|
||||
"개발자 추가 채용 검토",
|
||||
"외부 업체 협업 범위 논의"
|
||||
))
|
||||
.build())
|
||||
.todos(List.of(
|
||||
MeetingEndResponse.TodoSummary.builder()
|
||||
.title("개발자 채용 공고 작성")
|
||||
.build()
|
||||
))
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 정보 조회
|
||||
|
||||
+558
-99
@@ -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) {
|
||||
@@ -119,23 +126,13 @@ public class MinutesController {
|
||||
log.info("회의록 상세 조회 요청 - userId: {}, minutesId: {}", userId, minutesId);
|
||||
|
||||
try {
|
||||
// 캐시 확인
|
||||
MinutesDetailResponse cachedResponse = cacheService.getCachedMinutesDetail(minutesId);
|
||||
if (cachedResponse != null) {
|
||||
log.debug("캐시된 회의록 상세 반환 - minutesId: {}", minutesId);
|
||||
return ResponseEntity.ok(ApiResponse.success(cachedResponse));
|
||||
}
|
||||
|
||||
// 회의록 조회
|
||||
MinutesDTO minutesDTO = minutesService.getMinutesById(minutesId);
|
||||
|
||||
// 응답 DTO 생성
|
||||
MinutesDetailResponse response = convertToMinutesDetailResponse(minutesDTO);
|
||||
// Mock 데이터 생성 (프론트엔드 테스트용)
|
||||
MinutesDetailResponse response = createMockMinutesDetail(minutesId, userId);
|
||||
|
||||
// 캐시 저장
|
||||
cacheService.cacheMinutesDetail(minutesId, response);
|
||||
|
||||
log.info("회의록 상세 조회 성공 - minutesId: {}", minutesId);
|
||||
log.info("회의록 상세 조회 성공 (Mock) - minutesId: {}", minutesId);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -345,74 +342,536 @@ public class MinutesController {
|
||||
.build();
|
||||
}
|
||||
|
||||
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 회의록 상세 데이터 생성 (프로토타입 기반 - 대시보드/회의록 탭 구조)
|
||||
*/
|
||||
private MinutesDetailResponse createMockMinutesDetail(String minutesId, String userId) {
|
||||
return MinutesDetailResponse.builder()
|
||||
.minutesId(minutesDTO.getMinutesId())
|
||||
.title(minutesDTO.getTitle())
|
||||
.memo(minutesDTO.getMemo())
|
||||
.status(minutesDTO.getStatus())
|
||||
.version(minutesDTO.getVersion())
|
||||
.createdAt(minutesDTO.getCreatedAt())
|
||||
.lastModifiedAt(minutesDTO.getLastModifiedAt())
|
||||
.createdBy(minutesDTO.getCreatedBy())
|
||||
.lastModifiedBy(minutesDTO.getLastModifiedBy())
|
||||
.meeting(convertToMeetingInfo(minutesDTO.getMeeting()))
|
||||
.sections(convertToSectionInfoList(minutesDTO.getSectionsInfo()))
|
||||
.todos(convertToTodoInfoList(minutesDTO.getTodos()))
|
||||
.minutesId(minutesId)
|
||||
.title("2025년 1분기 제품 기획 회의록")
|
||||
.memo("본 회의는 AI 기반 회의록 자동화 서비스 개발을 위한 전략 회의입니다.")
|
||||
.status("FINALIZED")
|
||||
.version(3)
|
||||
.createdAt(LocalDateTime.of(2025, 10, 25, 14, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2025, 10, 25, 17, 30))
|
||||
.createdBy("김민준")
|
||||
.lastModifiedBy("김민준")
|
||||
.meeting(createMockMeetingInfo())
|
||||
.dashboard(createMockDashboardInfo())
|
||||
.agendas(createMockAgendaInfo())
|
||||
.build();
|
||||
}
|
||||
|
||||
private MinutesDetailResponse.MeetingInfo convertToMeetingInfo(MinutesDTO.MeetingInfo meetingInfo) {
|
||||
if (meetingInfo == null) return null;
|
||||
|
||||
/**
|
||||
* Mock 회의 정보 생성 (프로토타입 기반)
|
||||
*/
|
||||
private MinutesDetailResponse.MeetingInfo createMockMeetingInfo() {
|
||||
return MinutesDetailResponse.MeetingInfo.builder()
|
||||
.meetingId(meetingInfo.getMeetingId())
|
||||
.title(meetingInfo.getTitle())
|
||||
.scheduledAt(meetingInfo.getScheduledAt())
|
||||
.startedAt(meetingInfo.getStartedAt())
|
||||
.endedAt(meetingInfo.getEndedAt())
|
||||
.organizerId(meetingInfo.getOrganizerId())
|
||||
.organizerName(meetingInfo.getOrganizerName())
|
||||
.meetingId("meeting-001")
|
||||
.title("2025년 1분기 제품 기획 회의")
|
||||
.scheduledAt(LocalDateTime.of(2025, 10, 25, 14, 0))
|
||||
.startedAt(LocalDateTime.of(2025, 10, 25, 14, 0))
|
||||
.endedAt(LocalDateTime.of(2025, 10, 25, 15, 30))
|
||||
.organizerId("김민준")
|
||||
.organizerName("김민준")
|
||||
.location("본사 2층 대회의실")
|
||||
.durationMinutes(90)
|
||||
.participants(List.of(
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("김민준")
|
||||
.name("김민준")
|
||||
.role("작성자")
|
||||
.avatarColor("avatar-green")
|
||||
.build(),
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("박서연")
|
||||
.name("박서연")
|
||||
.role("참여자")
|
||||
.avatarColor("avatar-blue")
|
||||
.build(),
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("이준호")
|
||||
.name("이준호")
|
||||
.role("참여자")
|
||||
.avatarColor("avatar-yellow")
|
||||
.build(),
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("최유진")
|
||||
.name("최유진")
|
||||
.role("참여자")
|
||||
.avatarColor("avatar-pink")
|
||||
.build()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.SectionInfo> convertToSectionInfoList(
|
||||
List<MinutesDTO.SectionInfo> sections) {
|
||||
if (sections == null) return List.of();
|
||||
|
||||
return sections.stream()
|
||||
.map(section -> MinutesDetailResponse.SectionInfo.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.title(section.getTitle())
|
||||
.content(section.getContent())
|
||||
.orderIndex(section.getOrderIndex())
|
||||
.isLocked(section.isLocked())
|
||||
.isVerified(section.isVerified())
|
||||
.lockedBy(section.getLockedBy())
|
||||
.lockedAt(section.getLockedAt())
|
||||
.verifiedBy(section.getVerifiedBy())
|
||||
.verifiedAt(section.getVerifiedAt())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
/**
|
||||
* Mock 대시보드 정보 생성 (프로토타입 기반)
|
||||
*/
|
||||
private MinutesDetailResponse.DashboardInfo createMockDashboardInfo() {
|
||||
return MinutesDetailResponse.DashboardInfo.builder()
|
||||
.keyPoints(List.of(
|
||||
MinutesDetailResponse.KeyPoint.builder()
|
||||
.index(1)
|
||||
.content("AI 기반 회의록 자동화 서비스 출시 결정. 타겟은 중소기업 및 스타트업.")
|
||||
.build(),
|
||||
MinutesDetailResponse.KeyPoint.builder()
|
||||
.index(2)
|
||||
.content("주요 기능: 음성인식, AI 요약, Todo 자동 추출, 실시간 검증 및 협업.")
|
||||
.build(),
|
||||
MinutesDetailResponse.KeyPoint.builder()
|
||||
.index(3)
|
||||
.content("개발 기간 3개월 (Phase 1-3), 베타 출시일 2025년 12월 1일.")
|
||||
.build(),
|
||||
MinutesDetailResponse.KeyPoint.builder()
|
||||
.index(4)
|
||||
.content("프리 런칭 캠페인 11월 진행, 초기 100팀 무료 제공 후 유료 전환.")
|
||||
.build()
|
||||
))
|
||||
.keywords(List.of("#AI회의록", "#음성인식", "#협업도구", "#스타트업", "#베타출시"))
|
||||
.stats(MinutesDetailResponse.Statistics.builder()
|
||||
.participantCount(4)
|
||||
.durationMinutes(90)
|
||||
.agendaCount(3)
|
||||
.todoCount(5)
|
||||
.build())
|
||||
.decisions(List.of(
|
||||
MinutesDetailResponse.Decision.builder()
|
||||
.content("베타 버전 출시일: 2025년 12월 1일")
|
||||
.decidedBy("김민준")
|
||||
.decidedAt(LocalDateTime.of(2025, 10, 25, 15, 30))
|
||||
.background("개발 일정 및 시장 진입 시기를 고려하여 12월 초 출시가 최적. Q4 마무리 전 베타 피드백 확보 가능.")
|
||||
.build(),
|
||||
MinutesDetailResponse.Decision.builder()
|
||||
.content("타겟 고객: 중소기업 및 스타트업")
|
||||
.decidedBy("박서연")
|
||||
.decidedAt(LocalDateTime.of(2025, 10, 25, 14, 45))
|
||||
.background("사용자 인터뷰 결과, 중소기업과 스타트업이 회의록 작성에 가장 많은 시간을 소비하며 자동화 니즈가 높음.")
|
||||
.build()
|
||||
))
|
||||
.todoProgress(createMockTodoProgress())
|
||||
.relatedMinutes(createMockRelatedMinutes())
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.TodoInfo> convertToTodoInfoList(
|
||||
List<MinutesDTO.TodoInfo> todos) {
|
||||
if (todos == null) return List.of();
|
||||
/**
|
||||
* Mock Todo 진행상황 생성 (프로토타입 기반 - 간단한 텍스트)
|
||||
*/
|
||||
private MinutesDetailResponse.TodoProgress createMockTodoProgress() {
|
||||
List<MinutesDetailResponse.SimpleTodo> todos = List.of(
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-001")
|
||||
.title("API 명세서 작성")
|
||||
.assigneeName("이준호")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("HIGH")
|
||||
.dueDate(LocalDateTime.of(2025, 10, 23, 18, 0))
|
||||
.dueDayStatus("D-2")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-002")
|
||||
.title("데이터베이스 스키마 설계")
|
||||
.assigneeName("이준호")
|
||||
.status("OVERDUE")
|
||||
.priority("HIGH")
|
||||
.dueDate(LocalDateTime.of(2025, 10, 20, 18, 0))
|
||||
.dueDayStatus("D+1 (지연)")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-003")
|
||||
.title("UI 프로토타입 디자인")
|
||||
.assigneeName("최유진")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("MEDIUM")
|
||||
.dueDate(LocalDateTime.of(2025, 10, 28, 18, 0))
|
||||
.dueDayStatus("D-7")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-004")
|
||||
.title("사용자 피드백 분석")
|
||||
.assigneeName("김민준")
|
||||
.status("COMPLETED")
|
||||
.priority("MEDIUM")
|
||||
.dueDate(LocalDateTime.of(2025, 10, 19, 18, 0))
|
||||
.dueDayStatus("완료")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-005")
|
||||
.title("예산 편성안 검토")
|
||||
.assigneeName("김민준")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("HIGH")
|
||||
.dueDate(LocalDateTime.of(2025, 10, 22, 18, 0))
|
||||
.dueDayStatus("D-1")
|
||||
.build()
|
||||
);
|
||||
|
||||
int completedCount = (int) todos.stream().filter(t -> "COMPLETED".equals(t.getStatus())).count();
|
||||
|
||||
return todos.stream()
|
||||
.map(todo -> MinutesDetailResponse.TodoInfo.builder()
|
||||
.todoId(todo.getTodoId())
|
||||
.title(todo.getTitle())
|
||||
.description(todo.getDescription())
|
||||
.assigneeId(todo.getAssigneeId())
|
||||
.assigneeName(todo.getAssigneeName())
|
||||
.priority(todo.getPriority())
|
||||
.status(todo.getStatus())
|
||||
.dueDate(todo.getDueDate())
|
||||
.completedAt(todo.getCompletedAt())
|
||||
.completedBy(todo.getCompletedBy())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
return MinutesDetailResponse.TodoProgress.builder()
|
||||
.totalCount(todos.size())
|
||||
.completedCount(completedCount)
|
||||
.progressPercentage((completedCount * 100) / todos.size())
|
||||
.todos(todos)
|
||||
.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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 안건 정보 생성 (프로토타입 기반 - 회의록 탭)
|
||||
*/
|
||||
private List<MinutesDetailResponse.AgendaInfo> createMockAgendaInfo() {
|
||||
return List.of(
|
||||
MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId("agenda-001")
|
||||
.title("1. 신제품 기획 방향")
|
||||
.orderIndex(1)
|
||||
.isVerified(true)
|
||||
.verifiedBy("박서연")
|
||||
.verifiedAt(LocalDateTime.of(2025, 10, 25, 16, 30))
|
||||
.aiSummary(MinutesDetailResponse.AiSummary.builder()
|
||||
.content("신제품은 AI 기반 회의록 자동화 서비스로 결정. 타겟은 중소기업 및 스타트업이며, 주요 기능은 음성인식, AI 요약, Todo 추출입니다. 경쟁사 대비 차별점은 실시간 검증 및 협업 기능입니다.")
|
||||
.generatedAt(LocalDateTime.of(2025, 10, 25, 16, 30))
|
||||
.modifiedAt(LocalDateTime.of(2025, 10, 25, 17, 0))
|
||||
.build())
|
||||
.details(MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(List.of(
|
||||
"AI 기반 회의록 자동화 서비스 출시 결정",
|
||||
"타겟 고객: 중소기업, 스타트업",
|
||||
"주요 기능: 음성인식, AI 요약, Todo 자동 추출",
|
||||
"차별화 포인트: 실시간 검증, 협업 기능"
|
||||
))
|
||||
.decisions(List.of(
|
||||
"베타 버전 출시일: 2025년 12월 1일",
|
||||
"초기 목표 사용자: 100개 팀"
|
||||
))
|
||||
.build())
|
||||
.relatedMinutes(createMockRelatedMinutes().subList(0, 3))
|
||||
.build(),
|
||||
MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId("agenda-002")
|
||||
.title("2. 개발 일정 및 리소스")
|
||||
.orderIndex(2)
|
||||
.isVerified(true)
|
||||
.verifiedBy("이준호")
|
||||
.verifiedAt(LocalDateTime.of(2025, 10, 25, 16, 32))
|
||||
.aiSummary(MinutesDetailResponse.AiSummary.builder()
|
||||
.content("개발 기간은 3개월로 설정. 백엔드 2명, 프론트 2명, AI 엔지니어 1명 투입. 주간 스프린트로 진행하며, 2주마다 베타 테스트 실시.")
|
||||
.generatedAt(LocalDateTime.of(2025, 10, 25, 16, 32))
|
||||
.build())
|
||||
.details(MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(List.of(
|
||||
"Phase 1 (11월): 핵심 기능 개발 (음성인식, AI 요약)",
|
||||
"Phase 2 (12월): 협업 기능 개발 (검증, 공유)",
|
||||
"Phase 3 (1월): 베타 테스트 및 최적화"
|
||||
))
|
||||
.decisions(List.of(
|
||||
"백엔드 개발자 2명",
|
||||
"프론트엔드 개발자 2명",
|
||||
"AI 엔지니어 1명"
|
||||
))
|
||||
.build())
|
||||
.relatedMinutes(createMockRelatedMinutes().subList(1, 2))
|
||||
.build(),
|
||||
MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId("agenda-003")
|
||||
.title("3. 마케팅 전략")
|
||||
.orderIndex(3)
|
||||
.isVerified(true)
|
||||
.verifiedBy("최유진")
|
||||
.verifiedAt(LocalDateTime.of(2025, 10, 25, 16, 35))
|
||||
.aiSummary(MinutesDetailResponse.AiSummary.builder()
|
||||
.content("베타 출시 전 프리 런칭 캠페인 진행. 주요 채널은 LinkedIn 및 스타트업 커뮤니티. 초기 100팀 무료 제공 후 유료 전환 유도.")
|
||||
.generatedAt(LocalDateTime.of(2025, 10, 25, 16, 35))
|
||||
.build())
|
||||
.details(MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(List.of(
|
||||
"기간: 11월 1일 ~ 11월 30일",
|
||||
"채널: LinkedIn, Product Hunt, 스타트업 커뮤니티",
|
||||
"목표: 500명 사전 신청"
|
||||
))
|
||||
.decisions(List.of(
|
||||
"초기 100팀 무료 제공",
|
||||
"피드백 수집 및 개선",
|
||||
"1월부터 유료 전환"
|
||||
))
|
||||
.build())
|
||||
.relatedMinutes(List.of())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
|
||||
// Mock 데이터로 대체 (프로토타입용)
|
||||
return createMockMinutesDetail(minutesDTO.getMinutesId(), "user123");
|
||||
}
|
||||
|
||||
}
|
||||
-41
@@ -19,8 +19,6 @@ public class DashboardResponse {
|
||||
@Schema(description = "예정된 회의 목록")
|
||||
private final List<UpcomingMeetingResponse> upcomingMeetings;
|
||||
|
||||
@Schema(description = "진행 중 Todo 목록")
|
||||
private final List<ActiveTodoResponse> activeTodos;
|
||||
|
||||
@Schema(description = "최근 회의록 목록")
|
||||
private final List<RecentMinutesResponse> myMinutes;
|
||||
@@ -36,9 +34,6 @@ public class DashboardResponse {
|
||||
.upcomingMeetings(dto.getUpcomingMeetings().stream()
|
||||
.map(UpcomingMeetingResponse::from)
|
||||
.toList())
|
||||
.activeTodos(dto.getActiveTodos().stream()
|
||||
.map(ActiveTodoResponse::from)
|
||||
.toList())
|
||||
.myMinutes(dto.getMyMinutes().stream()
|
||||
.map(RecentMinutesResponse::from)
|
||||
.toList())
|
||||
@@ -84,39 +79,6 @@ public class DashboardResponse {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@Schema(description = "진행 중 Todo 정보")
|
||||
public static class ActiveTodoResponse {
|
||||
@Schema(description = "Todo ID", example = "660e8400-e29b-41d4-a716-446655440000")
|
||||
private final String todoId;
|
||||
|
||||
@Schema(description = "Todo 내용", example = "API 설계 문서 작성")
|
||||
private final String content;
|
||||
|
||||
@Schema(description = "마감일", example = "2025-01-30")
|
||||
private final String dueDate;
|
||||
|
||||
@Schema(description = "우선순위", example = "HIGH")
|
||||
private final String priority;
|
||||
|
||||
@Schema(description = "Todo 상태", example = "IN_PROGRESS")
|
||||
private final String status;
|
||||
|
||||
@Schema(description = "회의록 ID", example = "770e8400-e29b-41d4-a716-446655440000")
|
||||
private final String minutesId;
|
||||
|
||||
public static ActiveTodoResponse from(DashboardDTO.ActiveTodoDTO dto) {
|
||||
return ActiveTodoResponse.builder()
|
||||
.todoId(dto.getTodoId())
|
||||
.content(dto.getContent())
|
||||
.dueDate(dto.getDueDate())
|
||||
.priority(dto.getPriority())
|
||||
.status(dto.getStatus())
|
||||
.minutesId(dto.getMinutesId())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@@ -159,8 +121,6 @@ public class DashboardResponse {
|
||||
@Schema(description = "예정된 회의 수", example = "2")
|
||||
private final Integer upcomingMeetingsCount;
|
||||
|
||||
@Schema(description = "진행 중 Todo 수", example = "5")
|
||||
private final Integer activeTodosCount;
|
||||
|
||||
@Schema(description = "Todo 완료율", example = "68.5")
|
||||
private final Double todoCompletionRate;
|
||||
@@ -168,7 +128,6 @@ public class DashboardResponse {
|
||||
public static StatisticsResponse from(DashboardDTO.StatisticsDTO dto) {
|
||||
return StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(dto.getUpcomingMeetingsCount())
|
||||
.activeTodosCount(dto.getActiveTodosCount())
|
||||
.todoCompletionRate(dto.getTodoCompletionRate())
|
||||
.build();
|
||||
}
|
||||
|
||||
+127
-23
@@ -9,7 +9,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의록 상세 조회 응답 DTO
|
||||
* 회의록 상세 조회 응답 DTO (프로토타입 기반 - 대시보드/회의록 탭 구조)
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@@ -27,14 +27,14 @@ public class MinutesDetailResponse {
|
||||
private String createdBy;
|
||||
private String lastModifiedBy;
|
||||
|
||||
// 회의 정보
|
||||
// 회의 기본 정보
|
||||
private MeetingInfo meeting;
|
||||
|
||||
// 섹션 목록
|
||||
private List<SectionInfo> sections;
|
||||
// 대시보드 탭 정보
|
||||
private DashboardInfo dashboard;
|
||||
|
||||
// Todo 목록
|
||||
private List<TodoInfo> todos;
|
||||
// 회의록 탭 정보 (안건별 상세)
|
||||
private List<AgendaInfo> agendas;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@@ -48,39 +48,143 @@ public class MinutesDetailResponse {
|
||||
private LocalDateTime endedAt;
|
||||
private String organizerId;
|
||||
private String organizerName;
|
||||
private String location;
|
||||
private int durationMinutes;
|
||||
private List<Participant> participants;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SectionInfo {
|
||||
private String sectionId;
|
||||
private String title;
|
||||
public static class Participant {
|
||||
private String userId;
|
||||
private String name;
|
||||
private String role; // 작성자, 참여자
|
||||
private String avatarColor; // avatar-green, avatar-blue, etc.
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DashboardInfo {
|
||||
private List<KeyPoint> keyPoints; // 핵심내용
|
||||
private List<String> keywords; // 키워드 태그
|
||||
private Statistics stats; // 통계 정보
|
||||
private List<Decision> decisions; // 결정사항
|
||||
private TodoProgress todoProgress; // Todo 진행상황
|
||||
private List<RelatedMinutes> relatedMinutes; // 관련회의록
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class KeyPoint {
|
||||
private int index;
|
||||
private String content;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Statistics {
|
||||
private int participantCount;
|
||||
private int durationMinutes;
|
||||
private int agendaCount;
|
||||
private int todoCount;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Decision {
|
||||
private String content;
|
||||
private String decidedBy;
|
||||
private LocalDateTime decidedAt;
|
||||
private String background;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TodoProgress {
|
||||
private int totalCount;
|
||||
private int completedCount;
|
||||
private int progressPercentage;
|
||||
private List<SimpleTodo> todos; // 간단한 Todo 목록 (한줄 텍스트)
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class SimpleTodo {
|
||||
private String todoId;
|
||||
private String title; // 간단한 한줄 텍스트
|
||||
private String assigneeName;
|
||||
private String status; // IN_PROGRESS, COMPLETED, OVERDUE
|
||||
private String priority; // HIGH, MEDIUM, LOW
|
||||
private LocalDateTime dueDate;
|
||||
private String dueDayStatus; // D-2, D+1, 완료 등
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RelatedMinutes {
|
||||
private String minutesId;
|
||||
private String title;
|
||||
private LocalDateTime meetingDate;
|
||||
private String author;
|
||||
private int relevancePercentage;
|
||||
private String relevanceLevel; // HIGH, MEDIUM, LOW
|
||||
private String summary;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AgendaInfo {
|
||||
private String agendaId;
|
||||
private String title;
|
||||
private int orderIndex;
|
||||
private boolean isLocked;
|
||||
private boolean isVerified;
|
||||
private String lockedBy;
|
||||
private LocalDateTime lockedAt;
|
||||
private String verifiedBy;
|
||||
private LocalDateTime verifiedAt;
|
||||
|
||||
// AI 요약
|
||||
private AiSummary aiSummary;
|
||||
|
||||
// 안건 상세 내용
|
||||
private AgendaDetails details;
|
||||
|
||||
// 관련회의록
|
||||
private List<RelatedMinutes> relatedMinutes;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TodoInfo {
|
||||
private String todoId;
|
||||
private String title;
|
||||
private String description;
|
||||
private String assigneeId;
|
||||
private String assigneeName;
|
||||
private String priority;
|
||||
private String status;
|
||||
private LocalDateTime dueDate;
|
||||
private LocalDateTime completedAt;
|
||||
private String completedBy;
|
||||
public static class AiSummary {
|
||||
private String content;
|
||||
private LocalDateTime generatedAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class AgendaDetails {
|
||||
private List<String> discussions; // 논의 사항
|
||||
private List<String> decisions; // 결정 사항
|
||||
}
|
||||
}
|
||||
+15
@@ -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; // 사용자가 생성한 회의록 여부
|
||||
}
|
||||
}
|
||||
+223
-93
@@ -1,8 +1,14 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.DashboardReader;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.MinutesEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.TodoEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingParticipantJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.MinutesJpaRepository;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -11,6 +17,11 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 대시보드 Gateway 구현체
|
||||
@@ -22,106 +33,227 @@ import java.time.LocalDateTime;
|
||||
public class DashboardGateway implements DashboardReader {
|
||||
|
||||
private final MeetingJpaRepository meetingJpaRepository;
|
||||
private final MeetingParticipantJpaRepository meetingParticipantJpaRepository;
|
||||
private final MinutesJpaRepository minutesJpaRepository;
|
||||
private final TodoJpaRepository todoJpaRepository;
|
||||
|
||||
@Override
|
||||
public Dashboard getDashboardByUserId(String userId) {
|
||||
log.debug("Getting dashboard for user: {}", userId);
|
||||
log.info("대시보드 데이터 조회 시작 - userId: {}", userId);
|
||||
|
||||
// 회의 통계 조회
|
||||
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).size();
|
||||
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").size();
|
||||
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").size();
|
||||
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").size();
|
||||
// 1. 다가오는 회의 목록 조회 (향후 30일, 최대 10개)
|
||||
List<Meeting> upcomingMeetings = getUpcomingMeetings(userId);
|
||||
|
||||
// 2. 최근 회의록 목록 조회 (최근 7일, 최대 10개)
|
||||
List<Minutes> recentMinutes = getRecentMinutes(userId);
|
||||
|
||||
// 3. 통계 정보 계산 (최근 30일 기준)
|
||||
Dashboard.Statistics statistics = calculateStatistics(userId);
|
||||
|
||||
// 회의록 통계 조회
|
||||
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
|
||||
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "DRAFT".equals(m.getStatus()))
|
||||
.count();
|
||||
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "FINALIZED".equals(m.getStatus()))
|
||||
.count();
|
||||
|
||||
// Todo 통계 조회
|
||||
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
|
||||
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
|
||||
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
|
||||
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
|
||||
.filter(todo -> todo.getDueDate() != null
|
||||
&& LocalDate.now().isAfter(todo.getDueDate())
|
||||
&& !"COMPLETED".equals(todo.getStatus()))
|
||||
.count();
|
||||
|
||||
// 통계 객체 생성
|
||||
Dashboard.Statistics statistics = Dashboard.Statistics.builder()
|
||||
.totalMeetings((int) totalMeetings)
|
||||
.scheduledMeetings((int) scheduledMeetings)
|
||||
.inProgressMeetings((int) inProgressMeetings)
|
||||
.completedMeetings((int) completedMeetings)
|
||||
.totalMinutes((int) totalMinutes)
|
||||
.draftMinutes((int) draftMinutes)
|
||||
.finalizedMinutes((int) finalizedMinutes)
|
||||
.totalTodos((int) totalTodos)
|
||||
.pendingTodos((int) pendingTodos)
|
||||
.completedTodos((int) completedTodos)
|
||||
.overdueTodos((int) overdueTodos)
|
||||
.build();
|
||||
|
||||
// 대시보드 생성
|
||||
return Dashboard.builder()
|
||||
Dashboard dashboard = Dashboard.builder()
|
||||
.userId(userId)
|
||||
.period("7days")
|
||||
.upcomingMeetings(upcomingMeetings)
|
||||
.recentMinutes(recentMinutes)
|
||||
.assignedTodos(new ArrayList<>())
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
|
||||
log.info("대시보드 데이터 조회 완료 - userId: {}, 예정 회의: {}개, 최근 회의록: {}개",
|
||||
userId, upcomingMeetings.size(), recentMinutes.size());
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dashboard getDashboardByUserIdAndPeriod(String userId, String period) {
|
||||
log.debug("Getting dashboard for user: {} with period: {}", userId, period);
|
||||
log.info("기간별 대시보드 데이터 조회 시작 - userId: {}, period: {}", userId, period);
|
||||
|
||||
// 기간 계산
|
||||
// 기간에 따른 조회 범위 계산
|
||||
LocalDateTime startTime = calculateStartTime(period);
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
|
||||
// 기간 내 회의 통계 조회
|
||||
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
// 1. 기간 내 다가오는 회의 목록 조회
|
||||
List<Meeting> upcomingMeetings = getUpcomingMeetingsByPeriod(userId, startTime, endTime);
|
||||
|
||||
// 2. 기간 내 최근 회의록 목록 조회
|
||||
List<Minutes> recentMinutes = getRecentMinutesByPeriod(userId, startTime, endTime);
|
||||
|
||||
// 3. 기간별 통계 정보 계산
|
||||
Dashboard.Statistics statistics = calculateStatisticsByPeriod(userId, startTime, endTime);
|
||||
|
||||
Dashboard dashboard = Dashboard.builder()
|
||||
.userId(userId)
|
||||
.period(period)
|
||||
.upcomingMeetings(upcomingMeetings)
|
||||
.recentMinutes(recentMinutes)
|
||||
.assignedTodos(new ArrayList<>())
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
|
||||
log.info("기간별 대시보드 데이터 조회 완료 - userId: {}, period: {}", userId, period);
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 다가오는 회의 목록 조회
|
||||
*/
|
||||
private List<Meeting> getUpcomingMeetings(String userId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime endTime = now.plusDays(30); // 향후 30일
|
||||
|
||||
return getUpcomingMeetingsByPeriod(userId, now, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 다가오는 회의 목록 조회
|
||||
*/
|
||||
private List<Meeting> getUpcomingMeetingsByPeriod(String userId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
Set<String> userMeetingIds = new HashSet<>();
|
||||
|
||||
// 주최자로 참여하는 예정/진행중 회의 조회
|
||||
List<MeetingEntity> organizerMeetings = meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||
.filter(m -> userId.equals(m.getOrganizerId()))
|
||||
.filter(m -> "SCHEDULED".equals(m.getStatus()) || "IN_PROGRESS".equals(m.getStatus()))
|
||||
.toList();
|
||||
|
||||
organizerMeetings.forEach(m -> userMeetingIds.add(m.getMeetingId()));
|
||||
|
||||
// 참석자로 참여하는 예정/진행중 회의 조회
|
||||
List<String> participantMeetingIds = meetingParticipantJpaRepository.findByUserId(userId).stream()
|
||||
.map(p -> p.getMeetingId())
|
||||
.toList();
|
||||
|
||||
List<MeetingEntity> participantMeetings = meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||
.filter(m -> participantMeetingIds.contains(m.getMeetingId()))
|
||||
.filter(m -> "SCHEDULED".equals(m.getStatus()) || "IN_PROGRESS".equals(m.getStatus()))
|
||||
.toList();
|
||||
|
||||
participantMeetings.forEach(m -> userMeetingIds.add(m.getMeetingId()));
|
||||
|
||||
// 중복 제거된 회의 목록을 시간순 정렬하여 최대 10개만 반환
|
||||
return meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||
.filter(m -> userMeetingIds.contains(m.getMeetingId()))
|
||||
.filter(m -> "SCHEDULED".equals(m.getStatus()) || "IN_PROGRESS".equals(m.getStatus()))
|
||||
.sorted((m1, m2) -> m1.getScheduledAt().compareTo(m2.getScheduledAt()))
|
||||
.limit(10)
|
||||
.map(MeetingEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 회의록 목록 조회
|
||||
*/
|
||||
private List<Minutes> getRecentMinutes(String userId) {
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(7);
|
||||
return getRecentMinutesByPeriod(userId, startTime, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 최근 회의록 목록 조회
|
||||
*/
|
||||
private List<Minutes> getRecentMinutesByPeriod(String userId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
Set<String> userMinutesIds = new HashSet<>();
|
||||
|
||||
// 작성자로 참여한 회의록 조회
|
||||
List<MinutesEntity> createdMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> m.getCreatedAt().isAfter(startTime) && m.getCreatedAt().isBefore(endTime))
|
||||
.toList();
|
||||
|
||||
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()));
|
||||
|
||||
// 중복 제거 후 최종 수정 시간순 정렬하여 최대 10개만 반환
|
||||
return minutesJpaRepository.findAll().stream()
|
||||
.filter(m -> userMinutesIds.contains(m.getMinutesId()))
|
||||
.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); // 최신순
|
||||
})
|
||||
.limit(10)
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 통계 정보 계산
|
||||
*/
|
||||
private Dashboard.Statistics calculateStatistics(String userId) {
|
||||
LocalDateTime startTime = LocalDateTime.now().minusDays(30); // 최근 30일
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
|
||||
return calculateStatisticsByPeriod(userId, startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간별 통계 정보 계산
|
||||
*/
|
||||
private Dashboard.Statistics calculateStatisticsByPeriod(String userId, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
// 사용자가 관련된 모든 회의 ID 수집
|
||||
Set<String> userMeetingIds = new HashSet<>();
|
||||
|
||||
// 주최자로 참여한 회의
|
||||
meetingJpaRepository.findByOrganizerId(userId).forEach(m -> userMeetingIds.add(m.getMeetingId()));
|
||||
|
||||
// 참석자로 참여한 회의
|
||||
meetingParticipantJpaRepository.findByUserId(userId).stream()
|
||||
.map(p -> p.getMeetingId())
|
||||
.forEach(userMeetingIds::add);
|
||||
|
||||
// 기간 내 회의 통계
|
||||
List<MeetingEntity> periodMeetings = meetingJpaRepository.findByScheduledAtBetween(startTime, endTime).stream()
|
||||
.filter(m -> userMeetingIds.contains(m.getMeetingId()))
|
||||
.toList();
|
||||
|
||||
long totalMeetings = periodMeetings.size();
|
||||
long scheduledMeetings = periodMeetings.stream().filter(m -> "SCHEDULED".equals(m.getStatus())).count();
|
||||
long inProgressMeetings = periodMeetings.stream().filter(m -> "IN_PROGRESS".equals(m.getStatus())).count();
|
||||
long completedMeetings = periodMeetings.stream().filter(m -> "COMPLETED".equals(m.getStatus())).count();
|
||||
|
||||
// 회의록 통계 (사용자가 관련된 모든 회의록)
|
||||
Set<String> userMinutesIds = new HashSet<>();
|
||||
|
||||
// 작성자로 참여한 회의록
|
||||
minutesJpaRepository.findByCreatedBy(userId).forEach(m -> userMinutesIds.add(m.getMinutesId()));
|
||||
|
||||
// 참석한 회의의 회의록
|
||||
userMeetingIds.forEach(meetingId -> {
|
||||
minutesJpaRepository.findByMeetingId(meetingId).forEach(m -> userMinutesIds.add(m.getMinutesId()));
|
||||
});
|
||||
|
||||
List<MinutesEntity> userMinutes = minutesJpaRepository.findAll().stream()
|
||||
.filter(m -> userMinutesIds.contains(m.getMinutesId()))
|
||||
.toList();
|
||||
|
||||
long totalMinutes = userMinutes.size();
|
||||
long draftMinutes = userMinutes.stream().filter(m -> "DRAFT".equals(m.getStatus())).count();
|
||||
long finalizedMinutes = userMinutes.stream().filter(m -> "FINALIZED".equals(m.getStatus())).count();
|
||||
|
||||
// Todo 통계
|
||||
List<TodoEntity> userTodos = todoJpaRepository.findByAssigneeId(userId);
|
||||
long totalTodos = userTodos.size();
|
||||
long pendingTodos = userTodos.stream().filter(t -> "PENDING".equals(t.getStatus())).count();
|
||||
long completedTodos = userTodos.stream().filter(t -> "COMPLETED".equals(t.getStatus())).count();
|
||||
long overdueTodos = userTodos.stream()
|
||||
.filter(t -> t.getDueDate() != null
|
||||
&& LocalDate.now().isAfter(t.getDueDate())
|
||||
&& !"COMPLETED".equals(t.getStatus()))
|
||||
.count();
|
||||
|
||||
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").stream()
|
||||
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(endTime))
|
||||
.count();
|
||||
|
||||
// 회의록 통계 조회 (전체 기간)
|
||||
long totalMinutes = minutesJpaRepository.findByCreatedBy(userId).size();
|
||||
long draftMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "DRAFT".equals(m.getStatus()))
|
||||
.count();
|
||||
long finalizedMinutes = minutesJpaRepository.findByCreatedBy(userId).stream()
|
||||
.filter(m -> "FINALIZED".equals(m.getStatus()))
|
||||
.count();
|
||||
|
||||
// Todo 통계 조회 (전체 기간)
|
||||
long totalTodos = todoJpaRepository.findByAssigneeId(userId).size();
|
||||
long pendingTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "PENDING").size();
|
||||
long completedTodos = todoJpaRepository.findByAssigneeIdAndStatus(userId, "COMPLETED").size();
|
||||
long overdueTodos = todoJpaRepository.findByAssigneeId(userId).stream()
|
||||
.filter(todo -> todo.getDueDate() != null
|
||||
&& LocalDate.now().isAfter(todo.getDueDate())
|
||||
&& !"COMPLETED".equals(todo.getStatus()))
|
||||
.count();
|
||||
|
||||
// 통계 객체 생성
|
||||
Dashboard.Statistics statistics = Dashboard.Statistics.builder()
|
||||
return Dashboard.Statistics.builder()
|
||||
.totalMeetings((int) totalMeetings)
|
||||
.scheduledMeetings((int) scheduledMeetings)
|
||||
.inProgressMeetings((int) inProgressMeetings)
|
||||
@@ -134,13 +266,6 @@ public class DashboardGateway implements DashboardReader {
|
||||
.completedTodos((int) completedTodos)
|
||||
.overdueTodos((int) overdueTodos)
|
||||
.build();
|
||||
|
||||
// 대시보드 생성
|
||||
return Dashboard.builder()
|
||||
.userId(userId)
|
||||
.period(period)
|
||||
.statistics(statistics)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,12 +274,17 @@ public class DashboardGateway implements DashboardReader {
|
||||
private LocalDateTime calculateStartTime(String period) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
return switch (period.toUpperCase()) {
|
||||
case "WEEK" -> now.minusWeeks(1);
|
||||
case "MONTH" -> now.minusMonths(1);
|
||||
case "QUARTER" -> now.minusMonths(3);
|
||||
case "YEAR" -> now.minusYears(1);
|
||||
default -> now.minusMonths(1); // 기본값: 1개월
|
||||
return switch (period.toLowerCase()) {
|
||||
case "1day" -> now.minusDays(1);
|
||||
case "3days" -> now.minusDays(3);
|
||||
case "7days" -> now.minusDays(7);
|
||||
case "30days" -> now.minusDays(30);
|
||||
case "90days" -> now.minusDays(90);
|
||||
case "week" -> now.minusWeeks(1);
|
||||
case "month" -> now.minusMonths(1);
|
||||
case "quarter" -> now.minusMonths(3);
|
||||
case "year" -> now.minusYears(1);
|
||||
default -> now.minusDays(7); // 기본값: 7일
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -66,6 +66,8 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
.status(this.status)
|
||||
.version(this.version)
|
||||
.createdBy(this.createdBy)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.lastModifiedAt(this.getUpdatedAt())
|
||||
.finalizedBy(this.finalizedBy)
|
||||
.finalizedAt(this.finalizedAt)
|
||||
.build();
|
||||
|
||||
@@ -66,6 +66,8 @@ public class TodoEntity extends BaseTimeEntity {
|
||||
.dueDate(this.dueDate)
|
||||
.status(this.status)
|
||||
.priority(this.priority)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.lastModifiedAt(this.getUpdatedAt())
|
||||
.completedAt(this.completedAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.unicorn.hgzero.meeting.infra.mapper;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Dashboard;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.DashboardResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Dashboard 도메인 객체를 Response DTO로 변환하는 매퍼
|
||||
*/
|
||||
@Component
|
||||
public class DashboardResponseMapper {
|
||||
|
||||
/**
|
||||
* Dashboard 도메인 객체를 DashboardResponse로 변환
|
||||
*/
|
||||
public DashboardResponse toResponse(Dashboard dashboard) {
|
||||
if (dashboard == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DashboardResponse.builder()
|
||||
.upcomingMeetings(toUpcomingMeetingResponses(dashboard.getUpcomingMeetings()))
|
||||
.myMinutes(toRecentMinutesResponses(dashboard.getRecentMinutes()))
|
||||
.statistics(toStatisticsResponse(dashboard.getStatistics()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Meeting 목록을 UpcomingMeetingResponse 목록으로 변환
|
||||
*/
|
||||
private List<DashboardResponse.UpcomingMeetingResponse> toUpcomingMeetingResponses(List<Meeting> meetings) {
|
||||
if (meetings == null || meetings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return meetings.stream()
|
||||
.map(this::toUpcomingMeetingResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Meeting을 UpcomingMeetingResponse로 변환
|
||||
*/
|
||||
private DashboardResponse.UpcomingMeetingResponse toUpcomingMeetingResponse(Meeting meeting) {
|
||||
return DashboardResponse.UpcomingMeetingResponse.builder()
|
||||
.meetingId(meeting.getMeetingId())
|
||||
.title(meeting.getTitle())
|
||||
.startTime(meeting.getScheduledAt())
|
||||
.endTime(meeting.getEndTime())
|
||||
.location(meeting.getLocation())
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.status(meeting.getStatus())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Minutes 목록을 RecentMinutesResponse 목록으로 변환
|
||||
*/
|
||||
private List<DashboardResponse.RecentMinutesResponse> toRecentMinutesResponses(List<Minutes> minutesList) {
|
||||
if (minutesList == null || minutesList.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return minutesList.stream()
|
||||
.map(this::toRecentMinutesResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Minutes를 RecentMinutesResponse로 변환
|
||||
*/
|
||||
private DashboardResponse.RecentMinutesResponse toRecentMinutesResponse(Minutes minutes) {
|
||||
return DashboardResponse.RecentMinutesResponse.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.title(minutes.getTitle())
|
||||
.meetingDate(minutes.getCreatedAt())
|
||||
.status(minutes.getStatus())
|
||||
.participantCount(0) // Meeting 정보가 필요한데 현재 Minutes에 직접적인 참석자 정보가 없음
|
||||
.lastModified(minutes.getLastModifiedAt() != null ?
|
||||
minutes.getLastModifiedAt() : minutes.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard.Statistics를 StatisticsResponse로 변환
|
||||
*/
|
||||
private DashboardResponse.StatisticsResponse toStatisticsResponse(Dashboard.Statistics statistics) {
|
||||
if (statistics == null) {
|
||||
return DashboardResponse.StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(0)
|
||||
.todoCompletionRate(0.0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Todo 완료율 계산
|
||||
double todoCompletionRate = 0.0;
|
||||
int totalTodos = statistics.getTotalTodos() != null ? statistics.getTotalTodos() : 0;
|
||||
int completedTodos = statistics.getCompletedTodos() != null ? statistics.getCompletedTodos() : 0;
|
||||
|
||||
if (totalTodos > 0) {
|
||||
todoCompletionRate = (double) completedTodos / totalTodos * 100.0;
|
||||
}
|
||||
|
||||
return DashboardResponse.StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(statistics.getScheduledMeetings() != null ?
|
||||
statistics.getScheduledMeetings() : 0)
|
||||
.todoCompletionRate(todoCompletionRate)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user