mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-06-13 03:39:10 +00:00
for merge
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 안건 섹션 도메인 모델
|
||||
* agenda_sections 테이블과 매핑
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgendaSection {
|
||||
|
||||
/**
|
||||
* 안건 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 회의록 ID
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 안건 번호
|
||||
*/
|
||||
private Integer agendaNumber;
|
||||
|
||||
/**
|
||||
* 안건 제목
|
||||
*/
|
||||
private String agendaTitle;
|
||||
|
||||
/**
|
||||
* AI 요약 (짧은 버전)
|
||||
*/
|
||||
private String aiSummaryShort;
|
||||
|
||||
/**
|
||||
* 논의사항 (JSON 형태로 저장)
|
||||
*/
|
||||
private String discussions;
|
||||
|
||||
/**
|
||||
* 결정사항 (JSON 형태로 저장)
|
||||
*/
|
||||
private String decisions;
|
||||
|
||||
/**
|
||||
* 의견 (JSON 형태로 저장)
|
||||
*/
|
||||
private String opinions;
|
||||
|
||||
/**
|
||||
* 보류사항 (JSON 형태로 저장)
|
||||
*/
|
||||
private String pendingItems;
|
||||
|
||||
/**
|
||||
* 할일 목록 (JSON 형태로 저장)
|
||||
*/
|
||||
private String todos;
|
||||
|
||||
/**
|
||||
* 생성일시
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정일시
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ public class DashboardDTO {
|
||||
private final String location;
|
||||
private final Integer participantCount;
|
||||
private final String status;
|
||||
private final String userRole; // CREATOR, PARTICIPANT
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,6 +77,7 @@ public class DashboardDTO {
|
||||
private final String status;
|
||||
private final Integer participantCount;
|
||||
private final LocalDateTime lastModified;
|
||||
private final String userRole; // CREATOR, PARTICIPANT
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +95,8 @@ public class DashboardDTO {
|
||||
* Dashboard 도메인 객체로부터 DashboardDTO 생성
|
||||
*/
|
||||
public static DashboardDTO from(Dashboard dashboard) {
|
||||
String currentUserId = dashboard.getUserId();
|
||||
|
||||
return DashboardDTO.builder()
|
||||
.upcomingMeetings(dashboard.getUpcomingMeetings().stream()
|
||||
.map(meeting -> UpcomingMeetingDTO.builder()
|
||||
@@ -103,6 +107,7 @@ public class DashboardDTO {
|
||||
.location(null) // Meeting 도메인에 location이 없음
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.status(meeting.getStatus())
|
||||
.userRole(currentUserId.equals(meeting.getOrganizerId()) ? "CREATOR" : "PARTICIPANT")
|
||||
.build())
|
||||
.toList())
|
||||
.activeTodos(dashboard.getAssignedTodos().stream()
|
||||
@@ -124,6 +129,7 @@ public class DashboardDTO {
|
||||
.status(minutes.getStatus())
|
||||
.participantCount(0) // Minutes 도메인에 participantCount가 없음
|
||||
.lastModified(minutes.getLastModifiedAt())
|
||||
.userRole(currentUserId.equals(minutes.getCreatedBy()) ? "CREATOR" : "PARTICIPANT")
|
||||
.build())
|
||||
.toList())
|
||||
.statistics(StatisticsDTO.builder()
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의 안건 섹션 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgendaSectionService {
|
||||
|
||||
private final AgendaSectionRepository agendaSectionRepository;
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건 섹션 목록 조회
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건 섹션 도메인 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<AgendaSection> getAgendaSectionsByMinutesId(String minutesId) {
|
||||
log.info("안건 섹션 목록 조회 - minutesId: {}", minutesId);
|
||||
|
||||
List<AgendaSectionEntity> entities = agendaSectionRepository.findByMinutesIdOrderByAgendaNumber(minutesId);
|
||||
|
||||
return entities.stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건 섹션 목록 조회
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건 섹션 도메인 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<AgendaSection> getAgendaSectionsByMeetingId(String meetingId) {
|
||||
log.info("안건 섹션 목록 조회 - meetingId: {}", meetingId);
|
||||
|
||||
List<AgendaSectionEntity> entities = agendaSectionRepository.findByMeetingIdOrderByAgendaNumber(meetingId);
|
||||
|
||||
return entities.stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건 섹션 저장
|
||||
* @param agendaSection 안건 섹션 도메인
|
||||
* @return 저장된 안건 섹션
|
||||
*/
|
||||
@Transactional
|
||||
public AgendaSection saveAgendaSection(AgendaSection agendaSection) {
|
||||
log.info("안건 섹션 저장 - minutesId: {}, agendaTitle: {}",
|
||||
agendaSection.getMinutesId(), agendaSection.getAgendaTitle());
|
||||
|
||||
AgendaSectionEntity entity = AgendaSectionEntity.fromDomain(agendaSection);
|
||||
AgendaSectionEntity savedEntity = agendaSectionRepository.save(entity);
|
||||
|
||||
return savedEntity.toDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건 섹션 목록 저장
|
||||
* @param agendaSections 안건 섹션 도메인 목록
|
||||
* @return 저장된 안건 섹션 목록
|
||||
*/
|
||||
@Transactional
|
||||
public List<AgendaSection> saveAgendaSections(List<AgendaSection> agendaSections) {
|
||||
log.info("안건 섹션 목록 저장 - 개수: {}", agendaSections.size());
|
||||
|
||||
List<AgendaSectionEntity> entities = agendaSections.stream()
|
||||
.map(AgendaSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<AgendaSectionEntity> savedEntities = agendaSectionRepository.saveAll(entities);
|
||||
|
||||
return savedEntities.stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
+168
-596
@@ -6,11 +6,13 @@ import com.unicorn.hgzero.meeting.biz.domain.Meeting;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.MinutesSection;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.Todo;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.biz.dto.MinutesDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MinutesSectionService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.TodoService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.AgendaSectionService;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.request.UpdateMinutesRequest;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MinutesDetailResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MinutesListResponse;
|
||||
@@ -21,6 +23,7 @@ import com.unicorn.hgzero.meeting.biz.dto.AiAnalysisDTO;
|
||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesAnalysisRequestEvent;
|
||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesFinalizedEvent;
|
||||
import com.unicorn.hgzero.meeting.infra.event.dto.MinutesSectionDTO;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantReader;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
@@ -40,6 +43,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -66,6 +70,9 @@ public class MinutesController {
|
||||
private final MeetingService meetingService;
|
||||
private final TodoService todoService;
|
||||
private final AiServiceGateway aiServiceGateway;
|
||||
private final AgendaSectionService agendaSectionService;
|
||||
private final ParticipantReader participantReader;
|
||||
private final com.unicorn.hgzero.meeting.biz.usecase.out.TodoReader todoReader;
|
||||
|
||||
/**
|
||||
* 회의록 목록 조회
|
||||
@@ -481,144 +488,6 @@ public class MinutesController {
|
||||
.isCreatedByUser(true) // 현재는 작성자 기준으로만 조회하므로 true
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 데이터 생성 (프론트엔드 테스트용)
|
||||
*/
|
||||
private List<MinutesListResponse.MinutesItem> createMockMinutesList(String userId) {
|
||||
List<MinutesListResponse.MinutesItem> mockData = List.of(
|
||||
// 사용자가 생성한 회의록들
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-001")
|
||||
.title("2024년 1분기 성과리뷰 회의록")
|
||||
.meetingTitle("2024년 1분기 성과리뷰")
|
||||
.status("FINALIZED")
|
||||
.version(3)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 15, 14, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 15, 16, 30))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 15, 14, 0))
|
||||
.createdBy(userId)
|
||||
.lastModifiedBy(userId)
|
||||
.participantCount(8)
|
||||
.todoCount(5)
|
||||
.completedTodoCount(5)
|
||||
.completionRate(100)
|
||||
.isCreatedByUser(true)
|
||||
.build(),
|
||||
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-002")
|
||||
.title("신규 프로젝트 킥오프 회의록")
|
||||
.meetingTitle("신규 프로젝트 킥오프")
|
||||
.status("DRAFT")
|
||||
.version(1)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 20, 10, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 20, 11, 45))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 20, 10, 0))
|
||||
.createdBy(userId)
|
||||
.lastModifiedBy("user-002")
|
||||
.participantCount(6)
|
||||
.todoCount(8)
|
||||
.completedTodoCount(3)
|
||||
.completionRate(75)
|
||||
.isCreatedByUser(true)
|
||||
.build(),
|
||||
|
||||
// 사용자가 참석한 회의록들
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-003")
|
||||
.title("마케팅 전략 회의록")
|
||||
.meetingTitle("마케팅 전략 논의")
|
||||
.status("FINALIZED")
|
||||
.version(2)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 18, 15, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 18, 17, 0))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 18, 15, 0))
|
||||
.createdBy("user-003")
|
||||
.lastModifiedBy("user-003")
|
||||
.participantCount(5)
|
||||
.todoCount(4)
|
||||
.completedTodoCount(4)
|
||||
.completionRate(100)
|
||||
.isCreatedByUser(false)
|
||||
.build(),
|
||||
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-004")
|
||||
.title("기술 아키텍처 리뷰 회의록")
|
||||
.meetingTitle("기술 아키텍처 리뷰")
|
||||
.status("DRAFT")
|
||||
.version(1)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 22, 9, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 22, 10, 30))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 22, 9, 0))
|
||||
.createdBy("user-004")
|
||||
.lastModifiedBy("user-004")
|
||||
.participantCount(7)
|
||||
.todoCount(6)
|
||||
.completedTodoCount(2)
|
||||
.completionRate(60)
|
||||
.isCreatedByUser(false)
|
||||
.build(),
|
||||
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-005")
|
||||
.title("주간 스프린트 회고 회의록")
|
||||
.meetingTitle("주간 스프린트 회고")
|
||||
.status("FINALIZED")
|
||||
.version(1)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 25, 16, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 25, 17, 0))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 25, 16, 0))
|
||||
.createdBy("user-005")
|
||||
.lastModifiedBy("user-005")
|
||||
.participantCount(4)
|
||||
.todoCount(3)
|
||||
.completedTodoCount(3)
|
||||
.completionRate(100)
|
||||
.isCreatedByUser(false)
|
||||
.build(),
|
||||
|
||||
// 추가 더미 데이터들
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-006")
|
||||
.title("고객 피드백 분석 회의록")
|
||||
.meetingTitle("고객 피드백 분석")
|
||||
.status("DRAFT")
|
||||
.version(2)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 28, 14, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 28, 15, 20))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 28, 14, 0))
|
||||
.createdBy(userId)
|
||||
.lastModifiedBy(userId)
|
||||
.participantCount(5)
|
||||
.todoCount(7)
|
||||
.completedTodoCount(4)
|
||||
.completionRate(85)
|
||||
.isCreatedByUser(true)
|
||||
.build(),
|
||||
|
||||
MinutesListResponse.MinutesItem.builder()
|
||||
.minutesId("minutes-007")
|
||||
.title("보안 정책 수립 회의록")
|
||||
.meetingTitle("보안 정책 수립")
|
||||
.status("FINALIZED")
|
||||
.version(1)
|
||||
.createdAt(LocalDateTime.of(2024, 3, 12, 10, 0))
|
||||
.lastModifiedAt(LocalDateTime.of(2024, 3, 12, 12, 0))
|
||||
.meetingDate(LocalDateTime.of(2024, 3, 12, 10, 0))
|
||||
.createdBy("user-006")
|
||||
.lastModifiedBy("user-006")
|
||||
.participantCount(6)
|
||||
.todoCount(4)
|
||||
.completedTodoCount(4)
|
||||
.completionRate(100)
|
||||
.isCreatedByUser(false)
|
||||
.build()
|
||||
);
|
||||
|
||||
return mockData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 필터링
|
||||
@@ -706,179 +575,6 @@ public class MinutesController {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 회의록 상세 데이터 생성 (프로토타입 기반 - 대시보드/회의록 탭 구조)
|
||||
*/
|
||||
private MinutesDetailResponse createMockMinutesDetail(String minutesId, String userId) {
|
||||
return MinutesDetailResponse.builder()
|
||||
.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 회의 정보 생성 (프로토타입 기반)
|
||||
*/
|
||||
private MinutesDetailResponse.MeetingInfo createMockMeetingInfo() {
|
||||
return MinutesDetailResponse.MeetingInfo.builder()
|
||||
.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 MinutesDetailResponse.TodoProgress.builder()
|
||||
.totalCount(todos.size())
|
||||
.completedCount(completedCount)
|
||||
.progressPercentage((completedCount * 100) / todos.size())
|
||||
.todos(todos)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock 관련회의록 생성 (프로토타입 기반)
|
||||
@@ -915,92 +611,6 @@ public class MinutesController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
// 실제 회의 정보 조회
|
||||
@@ -1031,8 +641,8 @@ public class MinutesController {
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("실제 데이터 조회 실패, 기본값 사용 - minutesId: {}", minutesDTO.getMinutesId(), e);
|
||||
return buildFallbackResponse(minutesDTO);
|
||||
log.error("실제 데이터 조회 실패 - minutesId: {}", minutesDTO.getMinutesId(), e);
|
||||
throw new RuntimeException("회의록 상세 정보 조회에 실패했습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,44 +687,88 @@ public class MinutesController {
|
||||
*/
|
||||
private List<MinutesDetailResponse.Participant> buildParticipantList(String meetingId) {
|
||||
try {
|
||||
// 실제 참석자 조회 (현재는 기본값 반환)
|
||||
// TODO: MeetingService.getParticipants() 메소드 구현 필요
|
||||
// 실제 참석자 조회
|
||||
List<String> participantIds = participantReader.findParticipantsByMeetingId(meetingId);
|
||||
|
||||
if (participantIds.isEmpty()) {
|
||||
log.info("참석자가 없습니다 - meetingId: {}", meetingId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 임시로 기본 참석자 목록 반환
|
||||
return List.of(
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("user1")
|
||||
.name("회의 생성자")
|
||||
.role("생성자")
|
||||
.avatarColor("avatar-green")
|
||||
.build(),
|
||||
MinutesDetailResponse.Participant.builder()
|
||||
.userId("user2")
|
||||
.name("참여자")
|
||||
.role("참여자")
|
||||
.avatarColor("avatar-blue")
|
||||
.build()
|
||||
);
|
||||
// 참석자 정보를 MinutesDetailResponse.Participant로 변환
|
||||
return participantIds.stream()
|
||||
.map(userId -> MinutesDetailResponse.Participant.builder()
|
||||
.userId(userId)
|
||||
.name(getUserName(userId)) // TODO: User 서비스에서 실제 이름 조회
|
||||
.role(determineRole(userId, meetingId)) // 역할 결정
|
||||
.avatarColor(generateAvatarColor(userId)) // 아바타 색상 생성
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.warn("참석자 정보 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return List.of();
|
||||
log.error("참석자 정보 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 이름 조회 (임시 구현)
|
||||
* TODO: User 서비스에서 실제 사용자 정보 조회
|
||||
*/
|
||||
private String getUserName(String userId) {
|
||||
// 임시로 userId를 그대로 반환
|
||||
// 실제로는 User 서비스에서 조회해야 함
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 참석자 역할 결정
|
||||
*/
|
||||
private String determineRole(String userId, String meetingId) {
|
||||
try {
|
||||
// 회의 정보를 조회해서 주최자인지 확인
|
||||
var meeting = meetingService.getMeeting(meetingId);
|
||||
if (meeting.getOrganizerId().equals(userId)) {
|
||||
return "주최자";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("회의 정보 조회 실패로 역할 확인 불가 - meetingId: {}", meetingId, e);
|
||||
}
|
||||
return "참석자";
|
||||
}
|
||||
|
||||
/**
|
||||
* 아바타 색상 생성
|
||||
*/
|
||||
private String generateAvatarColor(String userId) {
|
||||
String[] colors = {"avatar-blue", "avatar-green", "avatar-purple", "avatar-orange", "avatar-red"};
|
||||
int hash = userId.hashCode();
|
||||
int index = Math.abs(hash) % colors.length;
|
||||
return colors[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건 정보 목록 구성
|
||||
*/
|
||||
private List<MinutesDetailResponse.AgendaInfo> buildAgendaInfoList(String minutesId) {
|
||||
try {
|
||||
// 실제 안건 조회
|
||||
var sections = minutesSectionService.getSectionsByMinutes(minutesId);
|
||||
|
||||
return sections.stream()
|
||||
.map(this::convertToAgendaInfo)
|
||||
.collect(Collectors.toList());
|
||||
// agenda_sections 테이블에서 실제 안건 조회
|
||||
var agendaSections = agendaSectionService.getAgendaSectionsByMinutesId(minutesId);
|
||||
|
||||
if (!agendaSections.isEmpty()) {
|
||||
// agenda_sections 데이터가 있으면 이를 사용
|
||||
return agendaSections.stream()
|
||||
.map(this::convertAgendaSectionToAgendaInfo)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
// agenda_sections 데이터가 없으면 기존 minutes_sections 데이터 사용
|
||||
var sections = minutesSectionService.getSectionsByMinutes(minutesId);
|
||||
return sections.stream()
|
||||
.map(this::convertToAgendaInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("안건 정보 조회 실패 - minutesId: {}", minutesId, e);
|
||||
return createSampleAgendas();
|
||||
log.error("안건 정보 조회 실패 - minutesId: {}", minutesId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,8 +777,8 @@ public class MinutesController {
|
||||
*/
|
||||
private MinutesDetailResponse.TodoProgress buildTodoProgress(String minutesId) {
|
||||
try {
|
||||
// 실제 Todo 목록 조회
|
||||
var todos = todoService.getTodosByMinutes(minutesId);
|
||||
// 실제 Todo 목록 조회 (상태 관계없이 모든 todos)
|
||||
List<com.unicorn.hgzero.meeting.biz.domain.Todo> todos = todoReader.findByMinutesId(minutesId);
|
||||
|
||||
int totalCount = todos.size();
|
||||
int completedCount = (int) todos.stream()
|
||||
@@ -1132,9 +786,18 @@ public class MinutesController {
|
||||
.count();
|
||||
|
||||
List<MinutesDetailResponse.SimpleTodo> simpleTodos = todos.stream()
|
||||
.map(this::convertToSimpleTodo)
|
||||
.map(todo -> MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId(todo.getTodoId())
|
||||
.title(todo.getTitle())
|
||||
.assigneeName(todo.getAssigneeId()) // 담당자 ID를 그대로 사용
|
||||
.dueDate(todo.getDueDate() != null ?
|
||||
todo.getDueDate().atStartOfDay() :
|
||||
null) // LocalDate를 LocalDateTime으로 변환
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.info("Todo 조회 성공 - minutesId: {}, totalCount: {}", minutesId, totalCount);
|
||||
|
||||
return MinutesDetailResponse.TodoProgress.builder()
|
||||
.totalCount(totalCount)
|
||||
.completedCount(completedCount)
|
||||
@@ -1142,8 +805,13 @@ public class MinutesController {
|
||||
.todos(simpleTodos)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.warn("Todo 정보 조회 실패 - minutesId: {}", minutesId, e);
|
||||
return createSampleTodoProgress();
|
||||
log.error("Todo 정보 조회 실패 - minutesId: {}", minutesId, e);
|
||||
return MinutesDetailResponse.TodoProgress.builder()
|
||||
.totalCount(0)
|
||||
.completedCount(0)
|
||||
.progressPercentage(0)
|
||||
.todos(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1188,30 +856,6 @@ public class MinutesController {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 폴백 응답 구성
|
||||
*/
|
||||
private MinutesDetailResponse buildFallbackResponse(MinutesDTO minutesDTO) {
|
||||
MinutesDetailResponse.MeetingInfo meetingInfo = buildDefaultMeetingInfo(minutesDTO);
|
||||
MinutesDetailResponse.TodoProgress todoProgress = createSampleTodoProgress();
|
||||
List<MinutesDetailResponse.AgendaInfo> agendas = createSampleAgendas();
|
||||
MinutesDetailResponse.DashboardInfo dashboardInfo = buildDashboardInfo(minutesDTO, agendas, todoProgress);
|
||||
|
||||
return MinutesDetailResponse.builder()
|
||||
.minutesId(minutesDTO.getMinutesId())
|
||||
.title(minutesDTO.getTitle())
|
||||
.memo(minutesDTO.getMemo() != null ? minutesDTO.getMemo() : "")
|
||||
.status(minutesDTO.getStatus())
|
||||
.version(minutesDTO.getVersion())
|
||||
.createdAt(minutesDTO.getCreatedAt())
|
||||
.lastModifiedAt(minutesDTO.getLastModifiedAt())
|
||||
.createdBy(minutesDTO.getCreatedBy())
|
||||
.lastModifiedBy(minutesDTO.getLastModifiedBy())
|
||||
.meeting(meetingInfo)
|
||||
.dashboard(dashboardInfo)
|
||||
.agendas(agendas)
|
||||
.build();
|
||||
}
|
||||
|
||||
// === 헬퍼 메소드들 ===
|
||||
|
||||
@@ -1234,7 +878,7 @@ public class MinutesController {
|
||||
private MinutesDetailResponse.AgendaInfo convertToAgendaInfo(Object section) {
|
||||
if (!(section instanceof MinutesSection)) {
|
||||
log.warn("MinutesSection이 아닌 객체가 전달됨: {}", section.getClass().getSimpleName());
|
||||
return createSampleAgenda("변환 실패 안건", 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
MinutesSection minutesSection = (MinutesSection) section;
|
||||
@@ -1265,6 +909,73 @@ public class MinutesController {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AgendaSection을 AgendaInfo로 변환
|
||||
*/
|
||||
private MinutesDetailResponse.AgendaInfo convertAgendaSectionToAgendaInfo(AgendaSection agendaSection) {
|
||||
// AI 요약 정보 구성
|
||||
MinutesDetailResponse.AiSummary aiSummary = MinutesDetailResponse.AiSummary.builder()
|
||||
.content(agendaSection.getAiSummaryShort() != null ? agendaSection.getAiSummaryShort() : "")
|
||||
.generatedAt(agendaSection.getCreatedAt() != null ? agendaSection.getCreatedAt() : LocalDateTime.now())
|
||||
.modifiedAt(agendaSection.getUpdatedAt() != null ? agendaSection.getUpdatedAt() : LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 안건 상세 내용 구성 - JSON 파싱
|
||||
List<String> discussionsList = parseJsonToList(agendaSection.getDiscussions());
|
||||
List<String> decisionsList = parseJsonToList(agendaSection.getDecisions());
|
||||
|
||||
MinutesDetailResponse.AgendaDetails details = MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(discussionsList)
|
||||
.decisions(decisionsList)
|
||||
.build();
|
||||
|
||||
return MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId(agendaSection.getId())
|
||||
.title(agendaSection.getAgendaTitle() != null ? agendaSection.getAgendaTitle() : "제목 없음")
|
||||
.orderIndex(agendaSection.getAgendaNumber() != null ? agendaSection.getAgendaNumber() : 1)
|
||||
.isVerified(true) // agenda_sections는 기본적으로 검증된 데이터
|
||||
.verifiedBy("AI")
|
||||
.verifiedAt(agendaSection.getCreatedAt())
|
||||
.aiSummary(aiSummary)
|
||||
.details(details)
|
||||
.relatedMinutes(new ArrayList<>()) // 관련 회의록은 별도 로직 필요
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 List<String>으로 파싱
|
||||
*/
|
||||
private List<String> parseJsonToList(String json) {
|
||||
if (json == null || json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
// 간단한 JSON 배열 파싱
|
||||
json = json.trim();
|
||||
if (json.startsWith("[") && json.endsWith("]")) {
|
||||
json = json.substring(1, json.length() - 1);
|
||||
if (json.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 쉼표로 분리하고 따옴표 제거
|
||||
return Arrays.stream(json.split(","))
|
||||
.map(s -> s.trim())
|
||||
.map(s -> s.startsWith("\"") && s.endsWith("\"") ? s.substring(1, s.length() - 1) : s)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// JSON 형식이 아니면 그대로 한 줄로 반환
|
||||
return List.of(json);
|
||||
} catch (Exception e) {
|
||||
log.warn("JSON 파싱 실패: {}", json, e);
|
||||
return List.of(json); // 파싱 실패시 원본 텍스트를 그대로 반환
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private MinutesDetailResponse.SimpleTodo convertToSimpleTodo(Object todo) {
|
||||
if (!(todo instanceof Todo)) {
|
||||
log.warn("Todo가 아닌 객체가 전달됨: {}", todo.getClass().getSimpleName());
|
||||
@@ -1431,10 +1142,7 @@ public class MinutesController {
|
||||
}
|
||||
}
|
||||
|
||||
// 샘플 데이터로 보완
|
||||
if (decisions.isEmpty()) {
|
||||
decisions = createSampleDecisions();
|
||||
}
|
||||
// 실제 데이터만 사용
|
||||
|
||||
return decisions;
|
||||
}
|
||||
@@ -1462,145 +1170,9 @@ public class MinutesController {
|
||||
);
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.Decision> createSampleDecisions() {
|
||||
return List.of(
|
||||
MinutesDetailResponse.Decision.builder()
|
||||
.content("베타 버전 출시일: 2025년 12월 1일")
|
||||
.decidedBy("김민준")
|
||||
.decidedAt(LocalDateTime.now().minusHours(2))
|
||||
.background("개발 일정 및 시장 진입 시기를 고려하여 12월 초 출시가 최적. Q4 마무리 전 베타 피드백 확보 가능.")
|
||||
.build(),
|
||||
MinutesDetailResponse.Decision.builder()
|
||||
.content("타겟 고객: 중소기업 및 스타트업")
|
||||
.decidedBy("박서연")
|
||||
.decidedAt(LocalDateTime.now().minusHours(3))
|
||||
.background("사용자 인터뷰 결과, 중소기업과 스타트업이 회의록 작성에 가장 많은 시간을 소비하며 자동화 니즈가 높음.")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private MinutesDetailResponse.TodoProgress createSampleTodoProgress() {
|
||||
List<MinutesDetailResponse.SimpleTodo> todos = List.of(
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-1")
|
||||
.title("데이터베이스 스키마 설계")
|
||||
.assigneeName("이준호")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("HIGH")
|
||||
.dueDate(LocalDateTime.now().minusDays(8))
|
||||
.dueDayStatus("D+8")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-2")
|
||||
.title("API 명세서 작성")
|
||||
.assigneeName("이준호")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("MEDIUM")
|
||||
.dueDate(LocalDateTime.now().minusDays(5))
|
||||
.dueDayStatus("D+5")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-3")
|
||||
.title("예산 편성안 검토")
|
||||
.assigneeName("김민준")
|
||||
.status("COMPLETED")
|
||||
.priority("HIGH")
|
||||
.dueDate(LocalDateTime.now().minusDays(6))
|
||||
.dueDayStatus("완료")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-4")
|
||||
.title("UI 프로토타입 디자인")
|
||||
.assigneeName("최유진")
|
||||
.status("IN_PROGRESS")
|
||||
.priority("MEDIUM")
|
||||
.dueDate(LocalDateTime.now())
|
||||
.dueDayStatus("D-Day")
|
||||
.build(),
|
||||
MinutesDetailResponse.SimpleTodo.builder()
|
||||
.todoId("todo-5")
|
||||
.title("사용자 피드백 분석")
|
||||
.assigneeName("김민준")
|
||||
.status("OVERDUE")
|
||||
.priority("LOW")
|
||||
.dueDate(LocalDateTime.now().minusDays(9))
|
||||
.dueDayStatus("D+9")
|
||||
.build()
|
||||
);
|
||||
|
||||
int totalCount = todos.size();
|
||||
int completedCount = (int) todos.stream()
|
||||
.filter(todo -> "COMPLETED".equals(todo.getStatus()))
|
||||
.count();
|
||||
|
||||
return MinutesDetailResponse.TodoProgress.builder()
|
||||
.totalCount(totalCount)
|
||||
.completedCount(completedCount)
|
||||
.progressPercentage(calculateProgressPercentage(totalCount, completedCount))
|
||||
.todos(todos)
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.RelatedMinutes> createSampleRelatedMinutes() {
|
||||
return List.of(
|
||||
MinutesDetailResponse.RelatedMinutes.builder()
|
||||
.minutesId("minutes-002")
|
||||
.title("AI 기능 개선 회의")
|
||||
.meetingDate(LocalDateTime.now().minusDays(2))
|
||||
.author("이준호")
|
||||
.relevancePercentage(92)
|
||||
.relevanceLevel("HIGH")
|
||||
.summary("AI 요약 정확도 개선 방안 논의. BERT 모델 도입 및 학습 데이터 확보 계획 수립.")
|
||||
.build(),
|
||||
MinutesDetailResponse.RelatedMinutes.builder()
|
||||
.minutesId("minutes-003")
|
||||
.title("개발 리소스 계획 회의")
|
||||
.meetingDate(LocalDateTime.now().minusDays(3))
|
||||
.author("김민준")
|
||||
.relevancePercentage(88)
|
||||
.relevanceLevel("MEDIUM")
|
||||
.summary("Q4 개발 리소스 현황 및 배분 계획. 신규 프로젝트 우선순위 협의.")
|
||||
.build(),
|
||||
MinutesDetailResponse.RelatedMinutes.builder()
|
||||
.minutesId("minutes-004")
|
||||
.title("경쟁사 분석 회의")
|
||||
.meetingDate(LocalDateTime.now().minusDays(5))
|
||||
.author("박서연")
|
||||
.relevancePercentage(78)
|
||||
.relevanceLevel("MEDIUM")
|
||||
.summary("경쟁사 A, B, C 분석 결과. 우리의 차별점은 실시간 협업 및 검증 기능.")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private List<MinutesDetailResponse.AgendaInfo> createSampleAgendas() {
|
||||
return List.of(
|
||||
createSampleAgenda("신제품 기획 방향", 1),
|
||||
createSampleAgenda("개발 일정 및 리소스", 2),
|
||||
createSampleAgenda("마케팅 전략", 3)
|
||||
);
|
||||
}
|
||||
|
||||
private MinutesDetailResponse.AgendaInfo createSampleAgenda(String title, int order) {
|
||||
return MinutesDetailResponse.AgendaInfo.builder()
|
||||
.agendaId("agenda-" + order)
|
||||
.title(order + ". " + title)
|
||||
.orderIndex(order)
|
||||
.isVerified(true)
|
||||
.verifiedBy("검증자")
|
||||
.verifiedAt(LocalDateTime.now().minusHours(1))
|
||||
.aiSummary(MinutesDetailResponse.AiSummary.builder()
|
||||
.content(title + "에 대한 AI 요약 내용입니다.")
|
||||
.generatedAt(LocalDateTime.now().minusHours(2))
|
||||
.modifiedAt(LocalDateTime.now().minusHours(1))
|
||||
.build())
|
||||
.details(MinutesDetailResponse.AgendaDetails.builder()
|
||||
.discussions(List.of("논의 사항 1", "논의 사항 2"))
|
||||
.decisions(List.of("결정 사항 1", "결정 사항 2"))
|
||||
.build())
|
||||
.relatedMinutes(createSampleRelatedMinutes().subList(0, 1))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 분석 결과로 응답 데이터 향상
|
||||
|
||||
+8
@@ -66,6 +66,9 @@ public class DashboardResponse {
|
||||
@Schema(description = "회의 상태", example = "SCHEDULED")
|
||||
private final String status;
|
||||
|
||||
@Schema(description = "사용자 역할", example = "CREATOR", allowableValues = {"CREATOR", "PARTICIPANT"})
|
||||
private final String userRole;
|
||||
|
||||
public static UpcomingMeetingResponse from(DashboardDTO.UpcomingMeetingDTO dto) {
|
||||
return UpcomingMeetingResponse.builder()
|
||||
.meetingId(dto.getMeetingId())
|
||||
@@ -75,6 +78,7 @@ public class DashboardResponse {
|
||||
.location(dto.getLocation())
|
||||
.participantCount(dto.getParticipantCount())
|
||||
.status(dto.getStatus())
|
||||
.userRole(dto.getUserRole())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -102,6 +106,9 @@ public class DashboardResponse {
|
||||
@Schema(description = "최종 수정 시간", example = "2025-01-23T16:30:00")
|
||||
private final LocalDateTime lastModified;
|
||||
|
||||
@Schema(description = "사용자 역할", example = "CREATOR", allowableValues = {"CREATOR", "PARTICIPANT"})
|
||||
private final String userRole;
|
||||
|
||||
public static RecentMinutesResponse from(DashboardDTO.RecentMinutesDTO dto) {
|
||||
return RecentMinutesResponse.builder()
|
||||
.minutesId(dto.getMinutesId())
|
||||
@@ -110,6 +117,7 @@ public class DashboardResponse {
|
||||
.status(dto.getStatus())
|
||||
.participantCount(dto.getParticipantCount())
|
||||
.lastModified(dto.getLastModified())
|
||||
.userRole(dto.getUserRole())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 회의 안건 섹션 엔티티
|
||||
* agenda_sections 테이블과 매핑
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "agenda_sections")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(length = 36)
|
||||
private String id;
|
||||
|
||||
@Column(name = "minutes_id", length = 36, nullable = false)
|
||||
private String minutesId;
|
||||
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "agenda_number", nullable = false)
|
||||
private Integer agendaNumber;
|
||||
|
||||
@Column(name = "agenda_title", length = 200, nullable = false)
|
||||
private String agendaTitle;
|
||||
|
||||
@Column(name = "ai_summary_short", columnDefinition = "TEXT")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Column(name = "discussions", columnDefinition = "TEXT")
|
||||
private String discussions;
|
||||
|
||||
@Column(name = "decisions", columnDefinition = "TEXT")
|
||||
private String decisions;
|
||||
|
||||
@Column(name = "opinions", columnDefinition = "TEXT")
|
||||
private String opinions;
|
||||
|
||||
@Column(name = "pending_items", columnDefinition = "TEXT")
|
||||
private String pendingItems;
|
||||
|
||||
@Column(name = "todos", columnDefinition = "TEXT")
|
||||
private String todos;
|
||||
|
||||
/**
|
||||
* 도메인 객체로 변환
|
||||
*/
|
||||
public AgendaSection toDomain() {
|
||||
return AgendaSection.builder()
|
||||
.id(this.id)
|
||||
.minutesId(this.minutesId)
|
||||
.meetingId(this.meetingId)
|
||||
.agendaNumber(this.agendaNumber)
|
||||
.agendaTitle(this.agendaTitle)
|
||||
.aiSummaryShort(this.aiSummaryShort)
|
||||
.discussions(this.discussions)
|
||||
.decisions(this.decisions)
|
||||
.opinions(this.opinions)
|
||||
.pendingItems(this.pendingItems)
|
||||
.todos(this.todos)
|
||||
.createdAt(this.getCreatedAt())
|
||||
.updatedAt(this.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 도메인 객체에서 엔티티 생성
|
||||
*/
|
||||
public static AgendaSectionEntity fromDomain(AgendaSection domain) {
|
||||
return AgendaSectionEntity.builder()
|
||||
.id(domain.getId())
|
||||
.minutesId(domain.getMinutesId())
|
||||
.meetingId(domain.getMeetingId())
|
||||
.agendaNumber(domain.getAgendaNumber())
|
||||
.agendaTitle(domain.getAgendaTitle())
|
||||
.aiSummaryShort(domain.getAiSummaryShort())
|
||||
.discussions(domain.getDiscussions())
|
||||
.decisions(domain.getDecisions())
|
||||
.opinions(domain.getOpinions())
|
||||
.pendingItems(domain.getPendingItems())
|
||||
.todos(domain.getTodos())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -20,7 +20,7 @@ import lombok.NoArgsConstructor;
|
||||
public class MinutesSectionEntity extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "section_id", length = 50)
|
||||
@Column(name = "id", length = 50)
|
||||
private String sectionId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.repository;
|
||||
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 회의 안건 섹션 리포지토리
|
||||
*/
|
||||
@Repository
|
||||
public interface AgendaSectionRepository extends JpaRepository<AgendaSectionEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건 섹션 목록 조회
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMinutesIdOrderByAgendaNumber(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건 섹션 목록 조회
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMeetingIdOrderByAgendaNumber(String meetingId);
|
||||
}
|
||||
+4
-91
@@ -1,15 +1,10 @@
|
||||
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.biz.dto.DashboardDTO;
|
||||
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로 변환하는 매퍼
|
||||
*/
|
||||
@@ -24,90 +19,8 @@ public class DashboardResponseMapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<DashboardResponse.UpcomingMeetingResponse> upcomingMeetings = toUpcomingMeetingResponses(dashboard.getUpcomingMeetings());
|
||||
List<DashboardResponse.RecentMinutesResponse> myMinutes = toRecentMinutesResponses(dashboard.getRecentMinutes());
|
||||
|
||||
return DashboardResponse.builder()
|
||||
.upcomingMeetings(upcomingMeetings)
|
||||
.myMinutes(myMinutes)
|
||||
.statistics(toStatisticsResponse(dashboard, upcomingMeetings.size()))
|
||||
.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 dashboard, int upcomingMeetingsCount) {
|
||||
Dashboard.Statistics statistics = dashboard.getStatistics();
|
||||
|
||||
if (statistics == null) {
|
||||
return DashboardResponse.StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(upcomingMeetingsCount)
|
||||
.draftMinutesCount(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
return DashboardResponse.StatisticsResponse.builder()
|
||||
.upcomingMeetingsCount(upcomingMeetingsCount)
|
||||
.draftMinutesCount(statistics.getDraftMinutes() != null ?
|
||||
statistics.getDraftMinutes() : 0)
|
||||
.build();
|
||||
// Dashboard 도메인을 DashboardDTO로 변환한 후 DashboardResponse로 변환
|
||||
DashboardDTO dashboardDTO = DashboardDTO.from(dashboard);
|
||||
return DashboardResponse.from(dashboardDTO);
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,22 @@ spring:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
hbm2ddl:
|
||||
auto: none
|
||||
temp:
|
||||
use_jdbc_metadata_defaults: false
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
javax:
|
||||
persistence:
|
||||
schema-generation:
|
||||
database:
|
||||
action: none
|
||||
scripts:
|
||||
action: none
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
ddl-auto: none
|
||||
|
||||
# Redis Configuration
|
||||
data:
|
||||
|
||||
Reference in New Issue
Block a user