Feat: 대시보드 조회 API 실제 데이터 연동

This commit is contained in:
cyjadela 2025-10-27 15:28:23 +09:00
parent b7f1352f86
commit 6a2574e9f5
96 changed files with 3381 additions and 212 deletions

View File

@ -8,7 +8,7 @@ spring:
datasource: datasource:
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb} url: jdbc:${DB_KIND:postgresql}://${DB_HOST:4.230.48.72}:${DB_PORT:5432}/${DB_NAME:meetingdb}
username: ${DB_USERNAME:hgzerouser} username: ${DB_USERNAME:hgzerouser}
password: ${DB_PASSWORD:} password: ${DB_PASSWORD:Hi5Jessica!}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
hikari: hikari:
maximum-pool-size: 20 maximum-pool-size: 20
@ -35,7 +35,7 @@ spring:
redis: redis:
host: ${REDIS_HOST:20.249.177.114} host: ${REDIS_HOST:20.249.177.114}
port: ${REDIS_PORT:6379} port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:} password: ${REDIS_PASSWORD:Hi5Jessica!}
timeout: 2000ms timeout: 2000ms
lettuce: lettuce:
pool: pool:
@ -51,7 +51,7 @@ server:
# JWT Configuration # JWT Configuration
jwt: jwt:
secret: ${JWT_SECRET:} secret: ${JWT_SECRET:hgzero-jwt-secret-key-for-dev-environment-only-do-not-use-in-production-minimum-256-bits}
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600} access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600}
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800} refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800}
@ -125,5 +125,11 @@ api:
# Azure EventHub Configuration # Azure EventHub Configuration
eventhub: eventhub:
connection-string: ${EVENTHUB_CONNECTION_STRING:} connection-string: ${EVENTHUB_CONNECTION_STRING:}
name: ${EVENTHUB_NAME:hgzero-eventhub-name} name: ${EVENTHUB_NAME:hgzero-events}
consumer-group: ${EVENTHUB_CONSUMER_GROUP:$Default} consumer-group: ${EVENTHUB_CONSUMER_GROUP:$Default}
# Azure Storage Configuration (for EventHub checkpoints)
azure:
storage:
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
container: ${AZURE_STORAGE_CONTAINER:hgzero-checkpoints}

View File

@ -0,0 +1,41 @@
-- 회의 참석자 테이블 생성
CREATE TABLE IF NOT EXISTS meeting_participants (
meeting_id VARCHAR(50) NOT NULL,
user_id VARCHAR(100) NOT NULL,
invitation_status VARCHAR(20) DEFAULT 'PENDING',
attended BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (meeting_id, user_id),
CONSTRAINT fk_meeting_participants_meeting
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id)
ON DELETE CASCADE
);
-- 기존 meetings 테이블의 participants 데이터를 meeting_participants 테이블로 마이그레이션
INSERT INTO meeting_participants (meeting_id, user_id, invitation_status, attended, created_at, updated_at)
SELECT
m.meeting_id,
TRIM(participant) as user_id,
'PENDING' as invitation_status,
FALSE as attended,
m.created_at,
m.updated_at
FROM meetings m
CROSS JOIN LATERAL unnest(string_to_array(m.participants, ',')) AS participant
WHERE m.participants IS NOT NULL AND m.participants != '';
-- meetings 테이블에서 participants 컬럼 삭제
ALTER TABLE meetings DROP COLUMN IF EXISTS participants;
-- 인덱스 생성
CREATE INDEX idx_meeting_participants_user_id ON meeting_participants(user_id);
CREATE INDEX idx_meeting_participants_invitation_status ON meeting_participants(invitation_status);
CREATE INDEX idx_meeting_participants_meeting_id_status ON meeting_participants(meeting_id, invitation_status);
-- 코멘트 추가
COMMENT ON TABLE meeting_participants IS '회의 참석자 정보';
COMMENT ON COLUMN meeting_participants.meeting_id IS '회의 ID';
COMMENT ON COLUMN meeting_participants.user_id IS '사용자 ID (이메일)';
COMMENT ON COLUMN meeting_participants.invitation_status IS '초대 상태 (PENDING, ACCEPTED, DECLINED)';
COMMENT ON COLUMN meeting_participants.attended IS '참석 여부';

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,10 @@
package com.unicorn.hgzero.meeting.infra.controller; package com.unicorn.hgzero.meeting.infra.controller;
import com.unicorn.hgzero.common.dto.ApiResponse; import com.unicorn.hgzero.common.dto.ApiResponse;
import com.unicorn.hgzero.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.dto.response.DashboardResponse;
import com.unicorn.hgzero.meeting.infra.mapper.DashboardResponseMapper;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@ -28,8 +30,11 @@ import java.util.List;
@Slf4j @Slf4j
public class DashboardController { public class DashboardController {
private final GetDashboardUseCase getDashboardUseCase;
private final DashboardResponseMapper dashboardResponseMapper;
/** /**
* 대시보드 데이터 조회 ( 데이터) * 대시보드 데이터 조회
* *
* @param userId 사용자 ID * @param userId 사용자 ID
* @return 대시보드 데이터 * @return 대시보드 데이터
@ -50,80 +55,61 @@ public class DashboardController {
log.info("대시보드 데이터 조회 요청 - userId: {}", userId); log.info("대시보드 데이터 조회 요청 - userId: {}", userId);
// 데이터 생성 try {
DashboardResponse mockResponse = createMockDashboardData(); // 실제 데이터 조회
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();
}
} }

View File

@ -19,8 +19,6 @@ public class DashboardResponse {
@Schema(description = "예정된 회의 목록") @Schema(description = "예정된 회의 목록")
private final List<UpcomingMeetingResponse> upcomingMeetings; private final List<UpcomingMeetingResponse> upcomingMeetings;
@Schema(description = "진행 중 Todo 목록")
private final List<ActiveTodoResponse> activeTodos;
@Schema(description = "최근 회의록 목록") @Schema(description = "최근 회의록 목록")
private final List<RecentMinutesResponse> myMinutes; private final List<RecentMinutesResponse> myMinutes;
@ -36,9 +34,6 @@ public class DashboardResponse {
.upcomingMeetings(dto.getUpcomingMeetings().stream() .upcomingMeetings(dto.getUpcomingMeetings().stream()
.map(UpcomingMeetingResponse::from) .map(UpcomingMeetingResponse::from)
.toList()) .toList())
.activeTodos(dto.getActiveTodos().stream()
.map(ActiveTodoResponse::from)
.toList())
.myMinutes(dto.getMyMinutes().stream() .myMinutes(dto.getMyMinutes().stream()
.map(RecentMinutesResponse::from) .map(RecentMinutesResponse::from)
.toList()) .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 @Getter
@Builder @Builder
@ -159,8 +121,6 @@ public class DashboardResponse {
@Schema(description = "예정된 회의 수", example = "2") @Schema(description = "예정된 회의 수", example = "2")
private final Integer upcomingMeetingsCount; private final Integer upcomingMeetingsCount;
@Schema(description = "진행 중 Todo 수", example = "5")
private final Integer activeTodosCount;
@Schema(description = "Todo 완료율", example = "68.5") @Schema(description = "Todo 완료율", example = "68.5")
private final Double todoCompletionRate; private final Double todoCompletionRate;
@ -168,7 +128,6 @@ public class DashboardResponse {
public static StatisticsResponse from(DashboardDTO.StatisticsDTO dto) { public static StatisticsResponse from(DashboardDTO.StatisticsDTO dto) {
return StatisticsResponse.builder() return StatisticsResponse.builder()
.upcomingMeetingsCount(dto.getUpcomingMeetingsCount()) .upcomingMeetingsCount(dto.getUpcomingMeetingsCount())
.activeTodosCount(dto.getActiveTodosCount())
.todoCompletionRate(dto.getTodoCompletionRate()) .todoCompletionRate(dto.getTodoCompletionRate())
.build(); .build();
} }

View File

@ -1,8 +1,14 @@
package com.unicorn.hgzero.meeting.infra.gateway; package com.unicorn.hgzero.meeting.infra.gateway;
import com.unicorn.hgzero.meeting.biz.domain.Dashboard; 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.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.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.MinutesJpaRepository;
import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository; import com.unicorn.hgzero.meeting.infra.gateway.repository.TodoJpaRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -11,6 +17,11 @@ 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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* 대시보드 Gateway 구현체 * 대시보드 Gateway 구현체
@ -22,106 +33,227 @@ import java.time.LocalDateTime;
public class DashboardGateway implements DashboardReader { public class DashboardGateway implements DashboardReader {
private final MeetingJpaRepository meetingJpaRepository; private final MeetingJpaRepository meetingJpaRepository;
private final MeetingParticipantJpaRepository meetingParticipantJpaRepository;
private final MinutesJpaRepository minutesJpaRepository; private final MinutesJpaRepository minutesJpaRepository;
private final TodoJpaRepository todoJpaRepository; private final TodoJpaRepository todoJpaRepository;
@Override @Override
public Dashboard getDashboardByUserId(String userId) { public Dashboard getDashboardByUserId(String userId) {
log.debug("Getting dashboard for user: {}", userId); log.info("대시보드 데이터 조회 시작 - userId: {}", userId);
// 회의 통계 조회 // 1. 다가오는 회의 목록 조회 (향후 30일, 최대 10개)
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).size(); List<Meeting> upcomingMeetings = getUpcomingMeetings(userId);
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").size();
long inProgressMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "IN_PROGRESS").size(); // 2. 최근 회의록 목록 조회 (최근 7일, 최대 10개)
long completedMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "COMPLETED").size(); List<Minutes> recentMinutes = getRecentMinutes(userId);
// 3. 통계 정보 계산 (최근 30일 기준)
Dashboard.Statistics statistics = calculateStatistics(userId);
// 회의록 통계 조회 Dashboard dashboard = Dashboard.builder()
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()
.userId(userId) .userId(userId)
.period("7days")
.upcomingMeetings(upcomingMeetings)
.recentMinutes(recentMinutes)
.assignedTodos(new ArrayList<>())
.statistics(statistics) .statistics(statistics)
.build(); .build();
log.info("대시보드 데이터 조회 완료 - userId: {}, 예정 회의: {}개, 최근 회의록: {}개",
userId, upcomingMeetings.size(), recentMinutes.size());
return dashboard;
} }
@Override @Override
public Dashboard getDashboardByUserIdAndPeriod(String userId, String period) { 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 startTime = calculateStartTime(period);
LocalDateTime endTime = LocalDateTime.now(); LocalDateTime endTime = LocalDateTime.now();
// 기간 회의 통계 조회 // 1. 기간 다가오는 회의 목록 조회
long totalMeetings = meetingJpaRepository.findByOrganizerId(userId).stream() List<Meeting> upcomingMeetings = getUpcomingMeetingsByPeriod(userId, startTime, endTime);
.filter(m -> m.getScheduledAt().isAfter(startTime) && m.getScheduledAt().isBefore(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(); .count();
long scheduledMeetings = meetingJpaRepository.findByOrganizerIdAndStatus(userId, "SCHEDULED").stream() return Dashboard.Statistics.builder()
.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()
.totalMeetings((int) totalMeetings) .totalMeetings((int) totalMeetings)
.scheduledMeetings((int) scheduledMeetings) .scheduledMeetings((int) scheduledMeetings)
.inProgressMeetings((int) inProgressMeetings) .inProgressMeetings((int) inProgressMeetings)
@ -134,13 +266,6 @@ public class DashboardGateway implements DashboardReader {
.completedTodos((int) completedTodos) .completedTodos((int) completedTodos)
.overdueTodos((int) overdueTodos) .overdueTodos((int) overdueTodos)
.build(); .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) { private LocalDateTime calculateStartTime(String period) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
return switch (period.toUpperCase()) { return switch (period.toLowerCase()) {
case "WEEK" -> now.minusWeeks(1); case "1day" -> now.minusDays(1);
case "MONTH" -> now.minusMonths(1); case "3days" -> now.minusDays(3);
case "QUARTER" -> now.minusMonths(3); case "7days" -> now.minusDays(7);
case "YEAR" -> now.minusYears(1); case "30days" -> now.minusDays(30);
default -> now.minusMonths(1); // 기본값: 1개월 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일
}; };
} }
} }

View File

@ -66,6 +66,8 @@ public class MinutesEntity extends BaseTimeEntity {
.status(this.status) .status(this.status)
.version(this.version) .version(this.version)
.createdBy(this.createdBy) .createdBy(this.createdBy)
.createdAt(this.getCreatedAt())
.lastModifiedAt(this.getUpdatedAt())
.finalizedBy(this.finalizedBy) .finalizedBy(this.finalizedBy)
.finalizedAt(this.finalizedAt) .finalizedAt(this.finalizedAt)
.build(); .build();

View File

@ -66,6 +66,8 @@ public class TodoEntity extends BaseTimeEntity {
.dueDate(this.dueDate) .dueDate(this.dueDate)
.status(this.status) .status(this.status)
.priority(this.priority) .priority(this.priority)
.createdAt(this.getCreatedAt())
.lastModifiedAt(this.getUpdatedAt())
.completedAt(this.completedAt) .completedAt(this.completedAt)
.build(); .build();
} }

View File

@ -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();
}
}