Feat: 회의록 상세 조회 API (mock) 구현

This commit is contained in:
cyjadela 2025-10-27 14:12:54 +09:00
parent 279bfa0758
commit 4f7046acfd
40 changed files with 2029 additions and 1585 deletions

File diff suppressed because it is too large Load Diff

View File

@ -126,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) {
@ -584,74 +574,304 @@ public class MinutesController {
.build();
}
private MinutesDetailResponse convertToMinutesDetailResponse(MinutesDTO minutesDTO) {
/**
* 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())
/**
* 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())
.collect(Collectors.toList());
.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()
);
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());
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 관련회의록 생성 (프로토타입 기반)
*/
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");
}
}

View File

@ -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; // 결정 사항
}
}