mirror of
https://github.com/hwanny1128/HGZero.git
synced 2026-01-21 09:06:23 +00:00
feat: Meeting Service AI 통합 API 개발 완료
## 구현 내용
- 참석자별 회의록 조회 API (GET /api/meetings/{meetingId}/ai/participant-minutes)
- 안건별 섹션 조회 API (GET /api/meetings/{meetingId}/ai/agenda-sections)
- 회의 통계 조회 API (GET /api/meetings/{meetingId}/ai/statistics)
## DB 스키마 변경
- V4 마이그레이션: agenda_sections 테이블에 todos JSON 컬럼 추가
- AI가 추출한 Todo를 안건별로 저장하는 구조
## 주요 특징
- AI Service가 한 번에 요약 + Todo 추출
- 프로토타입 기반 요구사항 반영 (불필요한 통계 제거)
- Todo 수를 agenda_sections의 todos 컬럼에서 집계
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
92e4863fc7
commit
79036128ec
@ -0,0 +1,138 @@
|
||||
package com.unicorn.hgzero.meeting.biz.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 도메인 모델
|
||||
* AI 요약 결과를 안건별로 구조화하여 저장
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgendaSection {
|
||||
|
||||
/**
|
||||
* 섹션 고유 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 회의록 ID (통합 회의록 참조)
|
||||
*/
|
||||
private String minutesId;
|
||||
|
||||
/**
|
||||
* 회의 ID
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 안건 번호 (1, 2, 3...)
|
||||
*/
|
||||
private Integer agendaNumber;
|
||||
|
||||
/**
|
||||
* 안건 제목
|
||||
*/
|
||||
private String agendaTitle;
|
||||
|
||||
/**
|
||||
* AI 생성 짧은 요약 (1줄, 20자 이내)
|
||||
*/
|
||||
private String aiSummaryShort;
|
||||
|
||||
/**
|
||||
* 논의 사항 (핵심 내용 3-5문장)
|
||||
*/
|
||||
private String discussions;
|
||||
|
||||
/**
|
||||
* 결정 사항 목록
|
||||
*/
|
||||
private List<String> decisions;
|
||||
|
||||
/**
|
||||
* 보류 사항 목록
|
||||
*/
|
||||
private List<String> pendingItems;
|
||||
|
||||
/**
|
||||
* 참석자별 의견
|
||||
*/
|
||||
private List<ParticipantOpinion> opinions;
|
||||
|
||||
/**
|
||||
* AI 추출 Todo 목록
|
||||
*/
|
||||
private List<TodoItem> todos;
|
||||
|
||||
/**
|
||||
* 생성 시간
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정 시간
|
||||
*/
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 참석자 의견 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class ParticipantOpinion {
|
||||
/**
|
||||
* 발언자 이름
|
||||
*/
|
||||
private String speaker;
|
||||
|
||||
/**
|
||||
* 의견 내용
|
||||
*/
|
||||
private String opinion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Todo 항목 내부 클래스
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class TodoItem {
|
||||
/**
|
||||
* Todo 제목
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 담당자
|
||||
*/
|
||||
private String assignee;
|
||||
|
||||
/**
|
||||
* 마감일 (YYYY-MM-DD)
|
||||
*/
|
||||
private String dueDate;
|
||||
|
||||
/**
|
||||
* 설명
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 우선순위 (HIGH, MEDIUM, LOW)
|
||||
*/
|
||||
private String priority;
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,11 @@ public class Minutes {
|
||||
*/
|
||||
private String meetingId;
|
||||
|
||||
/**
|
||||
* 작성자 사용자 ID (NULL: AI 통합 회의록, NOT NULL: 참석자별 회의록)
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 회의록 제목
|
||||
*/
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
package com.unicorn.hgzero.meeting.biz.service;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionWriter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 서비스
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional(readOnly = true)
|
||||
public class AgendaSectionService {
|
||||
|
||||
private final AgendaSectionReader agendaSectionReader;
|
||||
private final AgendaSectionWriter agendaSectionWriter;
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건별 섹션 목록 (안건 번호 순 정렬)
|
||||
*/
|
||||
public List<AgendaSection> getAgendaSectionsByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 안건별 섹션 조회: {}", meetingId);
|
||||
return agendaSectionReader.findByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
public List<AgendaSection> getAgendaSectionsByMinutes(String minutesId) {
|
||||
log.info("회의록 ID로 안건별 섹션 조회: {}", minutesId);
|
||||
return agendaSectionReader.findByMinutesId(minutesId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 안건별 섹션 일괄 저장
|
||||
*
|
||||
* @param sections 안건 섹션 목록
|
||||
* @return 저장된 안건 섹션 목록
|
||||
*/
|
||||
@Transactional
|
||||
public List<AgendaSection> saveAgendaSections(List<AgendaSection> sections) {
|
||||
log.info("안건별 섹션 일괄 저장: {} 개", sections.size());
|
||||
return agendaSectionWriter.saveAll(sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 전체 삭제
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteAgendaSectionsByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 안건별 섹션 전체 삭제: {}", meetingId);
|
||||
agendaSectionWriter.deleteByMeetingId(meetingId);
|
||||
}
|
||||
}
|
||||
@ -358,4 +358,30 @@ public class MinutesService implements
|
||||
.memo("") // 메모 필드는 추후 구현
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 참석자별 회의록 목록 (user_id IS NOT NULL)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public List<Minutes> getParticipantMinutesByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 참석자별 회의록 조회: {}", meetingId);
|
||||
return minutesReader.findParticipantMinutesByMeetingId(meetingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return AI 통합 회의록 (user_id IS NULL)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Minutes getConsolidatedMinutesByMeeting(String meetingId) {
|
||||
log.info("회의 ID로 AI 통합 회의록 조회: {}", meetingId);
|
||||
return minutesReader.findConsolidatedMinutesByMeetingId(meetingId)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 조회 인터페이스
|
||||
*/
|
||||
public interface AgendaSectionReader {
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건별 섹션 목록 (안건 번호 순 정렬)
|
||||
*/
|
||||
List<AgendaSection> findByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSection> findByMinutesId(String minutesId);
|
||||
|
||||
/**
|
||||
* 섹션 ID로 조회
|
||||
*
|
||||
* @param id 섹션 ID
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
Optional<AgendaSection> findById(String id);
|
||||
|
||||
/**
|
||||
* 회의 ID와 안건 번호로 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param agendaNumber 안건 번호
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
Optional<AgendaSection> findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.unicorn.hgzero.meeting.biz.usecase.out;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 저장 인터페이스
|
||||
*/
|
||||
public interface AgendaSectionWriter {
|
||||
|
||||
/**
|
||||
* 안건별 섹션 저장
|
||||
*
|
||||
* @param section 안건 섹션
|
||||
* @return 저장된 안건 섹션
|
||||
*/
|
||||
AgendaSection save(AgendaSection section);
|
||||
|
||||
/**
|
||||
* 안건별 섹션 일괄 저장
|
||||
*
|
||||
* @param sections 안건 섹션 목록
|
||||
* @return 저장된 안건 섹션 목록
|
||||
*/
|
||||
List<AgendaSection> saveAll(List<AgendaSection> sections);
|
||||
|
||||
/**
|
||||
* 안건별 섹션 삭제
|
||||
*
|
||||
* @param id 섹션 ID
|
||||
*/
|
||||
void delete(String id);
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 전체 삭제
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
*/
|
||||
void deleteByMeetingId(String meetingId);
|
||||
}
|
||||
@ -39,4 +39,21 @@ public interface MinutesReader {
|
||||
* 확정자 ID로 회의록 목록 조회
|
||||
*/
|
||||
List<Minutes> findByFinalizedBy(String finalizedBy);
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 참석자별 회의록 목록
|
||||
*/
|
||||
List<Minutes> findParticipantMinutesByMeetingId(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회 (user_id IS NULL)
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return AI 통합 회의록
|
||||
*/
|
||||
Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,233 @@
|
||||
package com.unicorn.hgzero.meeting.infra.controller;
|
||||
|
||||
import com.unicorn.hgzero.common.dto.ApiResponse;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
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.service.AgendaSectionService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MeetingService;
|
||||
import com.unicorn.hgzero.meeting.biz.service.MinutesService;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.ParticipantMinutesResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.AgendaSectionResponse;
|
||||
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingStatisticsResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 회의 AI 기능 API Controller
|
||||
* 회의 종료 후 AI 통합 회의록 생성을 위한 API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "Meeting AI", description = "회의 AI 통합 기능 API")
|
||||
public class MeetingAiController {
|
||||
|
||||
private final MinutesService minutesService;
|
||||
private final AgendaSectionService agendaSectionService;
|
||||
private final MeetingService meetingService;
|
||||
|
||||
@GetMapping("/{meetingId}/ai/participant-minutes")
|
||||
@Operation(
|
||||
summary = "참석자별 회의록 조회",
|
||||
description = "특정 회의의 모든 참석자가 작성한 회의록을 조회합니다. AI Service가 통합 회의록 생성 시 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<ParticipantMinutesResponse>> getParticipantMinutes(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("참석자별 회의록 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
List<Minutes> participantMinutes = minutesService.getParticipantMinutesByMeeting(meetingId);
|
||||
|
||||
List<ParticipantMinutesResponse.ParticipantMinutesItem> items = participantMinutes.stream()
|
||||
.map(this::convertToParticipantMinutesItem)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ParticipantMinutesResponse response = ParticipantMinutesResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.participantMinutes(items)
|
||||
.build();
|
||||
|
||||
log.info("참석자별 회의록 조회 성공 - meetingId: {}, count: {}", meetingId, items.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("참석자별 회의록 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("참석자별 회의록 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{meetingId}/ai/agenda-sections")
|
||||
@Operation(
|
||||
summary = "안건별 섹션 조회",
|
||||
description = "특정 회의의 안건별 AI 요약 섹션을 조회합니다. 회의 종료 화면에서 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<AgendaSectionResponse>> getAgendaSections(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("안건별 섹션 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
List<AgendaSection> sections = agendaSectionService.getAgendaSectionsByMeeting(meetingId);
|
||||
|
||||
List<AgendaSectionResponse.AgendaSectionItem> items = sections.stream()
|
||||
.map(this::convertToAgendaSectionItem)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
AgendaSectionResponse response = AgendaSectionResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.sections(items)
|
||||
.build();
|
||||
|
||||
log.info("안건별 섹션 조회 성공 - meetingId: {}, count: {}", meetingId, items.size());
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("안건별 섹션 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("안건별 섹션 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{meetingId}/ai/statistics")
|
||||
@Operation(
|
||||
summary = "회의 통계 조회",
|
||||
description = "특정 회의의 통계 정보를 조회합니다. 회의 종료 화면에서 사용합니다."
|
||||
)
|
||||
public ResponseEntity<ApiResponse<MeetingStatisticsResponse>> getMeetingStatistics(
|
||||
@Parameter(description = "회의 ID") @PathVariable String meetingId,
|
||||
@RequestHeader("X-User-Id") String userId) {
|
||||
|
||||
log.info("회의 통계 조회 요청 - meetingId: {}, userId: {}", meetingId, userId);
|
||||
|
||||
try {
|
||||
MeetingStatisticsResponse response = buildMeetingStatistics(meetingId);
|
||||
|
||||
log.info("회의 통계 조회 성공 - meetingId: {}", meetingId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("회의 통계 조회 실패 - meetingId: {}", meetingId, e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ApiResponse.errorWithType("회의 통계 조회에 실패했습니다"));
|
||||
}
|
||||
}
|
||||
|
||||
private ParticipantMinutesResponse.ParticipantMinutesItem convertToParticipantMinutesItem(Minutes minutes) {
|
||||
List<ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection> sections = null;
|
||||
if (minutes.getSections() != null) {
|
||||
sections = minutes.getSections().stream()
|
||||
.map(this::convertToMinutesSection)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return ParticipantMinutesResponse.ParticipantMinutesItem.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.status(minutes.getStatus())
|
||||
.version(minutes.getVersion())
|
||||
.createdBy(minutes.getCreatedBy())
|
||||
.createdAt(minutes.getCreatedAt())
|
||||
.lastModifiedAt(minutes.getLastModifiedAt())
|
||||
.sections(sections)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection convertToMinutesSection(MinutesSection section) {
|
||||
return ParticipantMinutesResponse.ParticipantMinutesItem.MinutesSection.builder()
|
||||
.sectionId(section.getSectionId())
|
||||
.title(section.getTitle())
|
||||
.type(section.getType())
|
||||
.content(section.getContent())
|
||||
.orderIndex(section.getOrder())
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgendaSectionResponse.AgendaSectionItem convertToAgendaSectionItem(AgendaSection section) {
|
||||
List<AgendaSectionResponse.AgendaSectionItem.ParticipantOpinion> opinions = null;
|
||||
if (section.getOpinions() != null) {
|
||||
opinions = section.getOpinions().stream()
|
||||
.map(op -> AgendaSectionResponse.AgendaSectionItem.ParticipantOpinion.builder()
|
||||
.speaker(op.getSpeaker())
|
||||
.opinion(op.getOpinion())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
List<AgendaSectionResponse.AgendaSectionItem.TodoItem> todos = null;
|
||||
if (section.getTodos() != null) {
|
||||
todos = section.getTodos().stream()
|
||||
.map(todo -> AgendaSectionResponse.AgendaSectionItem.TodoItem.builder()
|
||||
.title(todo.getTitle())
|
||||
.assignee(todo.getAssignee())
|
||||
.dueDate(todo.getDueDate())
|
||||
.description(todo.getDescription())
|
||||
.priority(todo.getPriority())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return AgendaSectionResponse.AgendaSectionItem.builder()
|
||||
.id(section.getId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.discussions(section.getDiscussions())
|
||||
.decisions(section.getDecisions())
|
||||
.pendingItems(section.getPendingItems())
|
||||
.opinions(opinions)
|
||||
.todos(todos)
|
||||
.createdAt(section.getCreatedAt())
|
||||
.updatedAt(section.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
private MeetingStatisticsResponse buildMeetingStatistics(String meetingId) {
|
||||
Meeting meeting = meetingService.getMeeting(meetingId);
|
||||
List<AgendaSection> sections = agendaSectionService.getAgendaSectionsByMeeting(meetingId);
|
||||
|
||||
// AI가 추출한 Todo 수 계산
|
||||
int todoCount = sections.stream()
|
||||
.mapToInt(s -> s.getTodos() != null ? s.getTodos().size() : 0)
|
||||
.sum();
|
||||
|
||||
// 회의 시간 계산
|
||||
Integer durationMinutes = null;
|
||||
if (meeting.getStartedAt() != null && meeting.getEndedAt() != null) {
|
||||
durationMinutes = (int) java.time.Duration.between(
|
||||
meeting.getStartedAt(),
|
||||
meeting.getEndedAt()
|
||||
).toMinutes();
|
||||
}
|
||||
|
||||
return MeetingStatisticsResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.meetingTitle(meeting.getTitle())
|
||||
.startTime(meeting.getStartedAt())
|
||||
.endTime(meeting.getEndedAt())
|
||||
.durationMinutes(durationMinutes)
|
||||
.participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
|
||||
.agendaCount(sections.size())
|
||||
.todoCount(todoCount)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 섹션 조회 응답 DTO
|
||||
* 회의 종료 화면에서 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "안건별 섹션 조회 응답")
|
||||
public class AgendaSectionResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "안건별 섹션 목록")
|
||||
private List<AgendaSectionItem> sections;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "안건별 섹션 항목")
|
||||
public static class AgendaSectionItem {
|
||||
|
||||
@Schema(description = "섹션 ID", example = "AGENDA-SEC-001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "회의록 ID", example = "MIN-CONSOLIDATED-001")
|
||||
private String minutesId;
|
||||
|
||||
@Schema(description = "안건 번호", example = "1")
|
||||
private Integer agendaNumber;
|
||||
|
||||
@Schema(description = "안건 제목", example = "Q1 마케팅 전략 수립")
|
||||
private String agendaTitle;
|
||||
|
||||
@Schema(description = "AI 생성 짧은 요약", example = "타겟 고객을 20-30대로 설정")
|
||||
private String aiSummaryShort;
|
||||
|
||||
@Schema(description = "논의 사항", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
|
||||
private String discussions;
|
||||
|
||||
@Schema(description = "결정 사항 목록")
|
||||
private List<String> decisions;
|
||||
|
||||
@Schema(description = "보류 사항 목록")
|
||||
private List<String> pendingItems;
|
||||
|
||||
@Schema(description = "참석자별 의견")
|
||||
private List<ParticipantOpinion> opinions;
|
||||
|
||||
@Schema(description = "AI 추출 Todo 목록")
|
||||
private List<TodoItem> todos;
|
||||
|
||||
@Schema(description = "생성 시간", example = "2025-01-20T16:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "수정 시간", example = "2025-01-20T16:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "참석자 의견")
|
||||
public static class ParticipantOpinion {
|
||||
|
||||
@Schema(description = "발언자 이름", example = "김민준")
|
||||
private String speaker;
|
||||
|
||||
@Schema(description = "의견 내용", example = "타겟 고객층을 명확히 설정하여 마케팅 전략 수립 필요")
|
||||
private String opinion;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Todo 항목")
|
||||
public static class TodoItem {
|
||||
|
||||
@Schema(description = "Todo 제목", example = "시장 조사 보고서 작성")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "담당자", example = "김민준")
|
||||
private String assignee;
|
||||
|
||||
@Schema(description = "마감일", example = "2025-02-15")
|
||||
private String dueDate;
|
||||
|
||||
@Schema(description = "설명", example = "20-30대 타겟 시장 조사")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "우선순위", example = "HIGH", allowableValues = {"HIGH", "MEDIUM", "LOW"})
|
||||
private String priority;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 회의 통계 조회 응답 DTO
|
||||
* 회의 종료 화면에서 통계 정보 표시
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의 통계 조회 응답")
|
||||
public class MeetingStatisticsResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "회의 제목", example = "Q1 마케팅 전략 회의")
|
||||
private String meetingTitle;
|
||||
|
||||
@Schema(description = "회의 시작 시간", example = "2025-01-20T14:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "회의 종료 시간", example = "2025-01-20T16:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Schema(description = "회의 총 시간 (분)", example = "120")
|
||||
private Integer durationMinutes;
|
||||
|
||||
@Schema(description = "참석자 수", example = "5")
|
||||
private Integer participantCount;
|
||||
|
||||
@Schema(description = "안건 수", example = "3")
|
||||
private Integer agendaCount;
|
||||
|
||||
@Schema(description = "Todo 수", example = "12")
|
||||
private Integer todoCount;
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
package com.unicorn.hgzero.meeting.infra.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 참석자별 회의록 조회 응답 DTO
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "참석자별 회의록 조회 응답")
|
||||
public class ParticipantMinutesResponse {
|
||||
|
||||
@Schema(description = "회의 ID", example = "MTG-2025-001")
|
||||
private String meetingId;
|
||||
|
||||
@Schema(description = "참석자별 회의록 목록")
|
||||
private List<ParticipantMinutesItem> participantMinutes;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "참석자별 회의록 항목")
|
||||
public static class ParticipantMinutesItem {
|
||||
|
||||
@Schema(description = "회의록 ID", example = "MIN-001")
|
||||
private String minutesId;
|
||||
|
||||
@Schema(description = "작성자 사용자 ID", example = "user@example.com")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "회의록 제목", example = "Q1 마케팅 전략 회의 - 김민준 작성")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "회의록 상태", example = "FINALIZED", allowableValues = {"DRAFT", "FINALIZED"})
|
||||
private String status;
|
||||
|
||||
@Schema(description = "버전", example = "1")
|
||||
private Integer version;
|
||||
|
||||
@Schema(description = "작성자 ID", example = "user@example.com")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "생성 시간", example = "2025-01-20T14:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Schema(description = "최종 수정 시간", example = "2025-01-20T15:30:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastModifiedAt;
|
||||
|
||||
@Schema(description = "회의록 섹션 목록")
|
||||
private List<MinutesSection> sections;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "회의록 섹션")
|
||||
public static class MinutesSection {
|
||||
|
||||
@Schema(description = "섹션 ID", example = "SEC-001")
|
||||
private String sectionId;
|
||||
|
||||
@Schema(description = "섹션 제목", example = "주요 논의 사항")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "섹션 유형", example = "DISCUSSION", allowableValues = {"DISCUSSION", "DECISION", "ACTION_ITEM", "NOTE"})
|
||||
private String type;
|
||||
|
||||
@Schema(description = "섹션 내용", example = "Q1 마케팅 캠페인 방향성에 대한 논의가 진행되었습니다...")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "섹션 순서", example = "1")
|
||||
private Integer orderIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway;
|
||||
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionReader;
|
||||
import com.unicorn.hgzero.meeting.biz.usecase.out.AgendaSectionWriter;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.entity.AgendaSectionEntity;
|
||||
import com.unicorn.hgzero.meeting.infra.gateway.repository.AgendaSectionJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 Gateway 구현체
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional(readOnly = true)
|
||||
public class AgendaSectionGateway implements AgendaSectionReader, AgendaSectionWriter {
|
||||
|
||||
private final AgendaSectionJpaRepository repository;
|
||||
|
||||
@Override
|
||||
public List<AgendaSection> findByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 안건별 섹션 조회: {}", meetingId);
|
||||
return repository.findByMeetingIdOrderByAgendaNumberAsc(meetingId).stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgendaSection> findByMinutesId(String minutesId) {
|
||||
log.debug("회의록 ID로 안건별 섹션 조회: {}", minutesId);
|
||||
return repository.findByMinutesIdOrderByAgendaNumberAsc(minutesId).stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgendaSection> findById(String id) {
|
||||
log.debug("ID로 안건별 섹션 조회: {}", id);
|
||||
return repository.findById(id)
|
||||
.map(AgendaSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgendaSection> findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber) {
|
||||
log.debug("회의 ID와 안건 번호로 섹션 조회: meetingId={}, agendaNumber={}", meetingId, agendaNumber);
|
||||
return Optional.ofNullable(repository.findByMeetingIdAndAgendaNumber(meetingId, agendaNumber))
|
||||
.map(AgendaSectionEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public AgendaSection save(AgendaSection section) {
|
||||
log.debug("안건별 섹션 저장: {}", section.getId());
|
||||
AgendaSectionEntity entity = AgendaSectionEntity.fromDomain(section);
|
||||
AgendaSectionEntity saved = repository.save(entity);
|
||||
return saved.toDomain();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public List<AgendaSection> saveAll(List<AgendaSection> sections) {
|
||||
log.debug("안건별 섹션 일괄 저장: {} 개", sections.size());
|
||||
List<AgendaSectionEntity> entities = sections.stream()
|
||||
.map(AgendaSectionEntity::fromDomain)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<AgendaSectionEntity> saved = repository.saveAll(entities);
|
||||
return saved.stream()
|
||||
.map(AgendaSectionEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(String id) {
|
||||
log.debug("안건별 섹션 삭제: {}", id);
|
||||
repository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 안건별 섹션 전체 삭제: {}", meetingId);
|
||||
repository.deleteByMeetingId(meetingId);
|
||||
}
|
||||
}
|
||||
@ -64,6 +64,21 @@ public class MinutesGateway implements MinutesReader, MinutesWriter {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Minutes> findParticipantMinutesByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 참석자별 회의록 조회: {}", meetingId);
|
||||
return minutesJpaRepository.findByMeetingIdAndUserIdIsNotNull(meetingId).stream()
|
||||
.map(MinutesEntity::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Minutes> findConsolidatedMinutesByMeetingId(String meetingId) {
|
||||
log.debug("회의 ID로 AI 통합 회의록 조회: {}", meetingId);
|
||||
return minutesJpaRepository.findByMeetingIdAndUserIdIsNull(meetingId)
|
||||
.map(MinutesEntity::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Minutes save(Minutes minutes) {
|
||||
// 기존 엔티티 조회 (update) 또는 새로 생성 (insert)
|
||||
|
||||
@ -0,0 +1,132 @@
|
||||
package com.unicorn.hgzero.meeting.infra.gateway.entity;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.unicorn.hgzero.common.entity.BaseTimeEntity;
|
||||
import com.unicorn.hgzero.meeting.biz.domain.AgendaSection;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 Entity
|
||||
* PostgreSQL JSON 컬럼을 사용하여 구조화된 데이터 저장
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "agenda_sections")
|
||||
@Getter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AgendaSectionEntity extends BaseTimeEntity {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Id
|
||||
@Column(name = "id", 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 = "json")
|
||||
private String decisionsJson;
|
||||
|
||||
@Column(name = "pending_items", columnDefinition = "json")
|
||||
private String pendingItemsJson;
|
||||
|
||||
@Column(name = "opinions", columnDefinition = "json")
|
||||
private String opinionsJson;
|
||||
|
||||
@Column(name = "todos", columnDefinition = "json")
|
||||
private String todosJson;
|
||||
|
||||
/**
|
||||
* Domain 객체로 변환
|
||||
*/
|
||||
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(parseJsonToList(this.decisionsJson, new TypeReference<List<String>>() {}))
|
||||
.pendingItems(parseJsonToList(this.pendingItemsJson, new TypeReference<List<String>>() {}))
|
||||
.opinions(parseJsonToList(this.opinionsJson, new TypeReference<List<AgendaSection.ParticipantOpinion>>() {}))
|
||||
.todos(parseJsonToList(this.todosJson, new TypeReference<List<AgendaSection.TodoItem>>() {}))
|
||||
.createdAt(this.getCreatedAt())
|
||||
.updatedAt(this.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain 객체에서 Entity 생성
|
||||
*/
|
||||
public static AgendaSectionEntity fromDomain(AgendaSection section) {
|
||||
return AgendaSectionEntity.builder()
|
||||
.id(section.getId())
|
||||
.minutesId(section.getMinutesId())
|
||||
.meetingId(section.getMeetingId())
|
||||
.agendaNumber(section.getAgendaNumber())
|
||||
.agendaTitle(section.getAgendaTitle())
|
||||
.aiSummaryShort(section.getAiSummaryShort())
|
||||
.discussions(section.getDiscussions())
|
||||
.decisionsJson(toJson(section.getDecisions()))
|
||||
.pendingItemsJson(toJson(section.getPendingItems()))
|
||||
.opinionsJson(toJson(section.getOpinions()))
|
||||
.todosJson(toJson(section.getTodos()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 객체를 JSON 문자열로 변환
|
||||
*/
|
||||
private static String toJson(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("JSON 변환 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열을 리스트로 변환
|
||||
*/
|
||||
private static <T> List<T> parseJsonToList(String json, TypeReference<List<T>> typeReference) {
|
||||
if (json == null || json.trim().isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, typeReference);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("JSON 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,9 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
@Column(name = "meeting_id", length = 50, nullable = false)
|
||||
private String meetingId;
|
||||
|
||||
@Column(name = "user_id", length = 100)
|
||||
private String userId;
|
||||
|
||||
@Column(name = "title", length = 200, nullable = false)
|
||||
private String title;
|
||||
|
||||
@ -59,6 +62,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
return Minutes.builder()
|
||||
.minutesId(this.minutesId)
|
||||
.meetingId(this.meetingId)
|
||||
.userId(this.userId)
|
||||
.title(this.title)
|
||||
.sections(this.sections.stream()
|
||||
.map(MinutesSectionEntity::toDomain)
|
||||
@ -77,6 +81,7 @@ public class MinutesEntity extends BaseTimeEntity {
|
||||
return MinutesEntity.builder()
|
||||
.minutesId(minutes.getMinutesId())
|
||||
.meetingId(minutes.getMeetingId())
|
||||
.userId(minutes.getUserId())
|
||||
.title(minutes.getTitle())
|
||||
.sections(minutes.getSections() != null
|
||||
? minutes.getSections().stream()
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 안건별 회의록 섹션 JPA Repository
|
||||
*/
|
||||
@Repository
|
||||
public interface AgendaSectionJpaRepository extends JpaRepository<AgendaSectionEntity, String> {
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 조회
|
||||
* 안건 번호 순으로 정렬
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMeetingIdOrderByAgendaNumberAsc(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의록 ID로 안건별 섹션 조회
|
||||
*
|
||||
* @param minutesId 회의록 ID
|
||||
* @return 안건별 섹션 목록
|
||||
*/
|
||||
List<AgendaSectionEntity> findByMinutesIdOrderByAgendaNumberAsc(String minutesId);
|
||||
|
||||
/**
|
||||
* 회의 ID와 안건 번호로 섹션 조회
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
* @param agendaNumber 안건 번호
|
||||
* @return 안건 섹션
|
||||
*/
|
||||
AgendaSectionEntity findByMeetingIdAndAgendaNumber(String meetingId, Integer agendaNumber);
|
||||
|
||||
/**
|
||||
* 회의 ID로 안건별 섹션 삭제
|
||||
*
|
||||
* @param meetingId 회의 ID
|
||||
*/
|
||||
void deleteByMeetingId(String meetingId);
|
||||
}
|
||||
@ -42,4 +42,15 @@ public interface MinutesJpaRepository extends JpaRepository<MinutesEntity, Strin
|
||||
* 회의 ID와 버전으로 회의록 조회
|
||||
*/
|
||||
Optional<MinutesEntity> findByMeetingIdAndVersion(String meetingId, Integer version);
|
||||
|
||||
/**
|
||||
* 회의 ID로 참석자별 회의록 조회 (user_id IS NOT NULL)
|
||||
* AI Service가 통합 회의록 생성 시 사용
|
||||
*/
|
||||
List<MinutesEntity> findByMeetingIdAndUserIdIsNotNull(String meetingId);
|
||||
|
||||
/**
|
||||
* 회의 ID로 AI 통합 회의록 조회 (user_id IS NULL)
|
||||
*/
|
||||
Optional<MinutesEntity> findByMeetingIdAndUserIdIsNull(String meetingId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
-- ========================================
|
||||
-- V4: agenda_sections 테이블에 todos 컬럼 추가
|
||||
-- ========================================
|
||||
-- 작성일: 2025-10-28
|
||||
-- 설명: AI가 추출한 Todo를 안건별 섹션에 저장
|
||||
|
||||
-- ========================================
|
||||
-- 1. agenda_sections 테이블에 todos 컬럼 추가
|
||||
-- ========================================
|
||||
|
||||
ALTER TABLE agenda_sections
|
||||
ADD COLUMN IF NOT EXISTS todos JSON;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN agenda_sections.todos IS 'AI 추출 Todo 목록 (JSON: [{title, assignee, dueDate, description, priority}])';
|
||||
|
||||
-- ========================================
|
||||
-- 2. 샘플 데이터 구조 (참고용)
|
||||
-- ========================================
|
||||
--
|
||||
-- todos JSON 구조:
|
||||
-- [
|
||||
-- {
|
||||
-- "title": "시장 조사 보고서 작성",
|
||||
-- "assignee": "김민준",
|
||||
-- "dueDate": "2025-02-15",
|
||||
-- "description": "20-30대 타겟 시장 조사",
|
||||
-- "priority": "HIGH"
|
||||
-- },
|
||||
-- {
|
||||
-- "title": "UI/UX 개선안 초안 작성",
|
||||
-- "assignee": "이서연",
|
||||
-- "dueDate": "2025-02-20",
|
||||
-- "description": "모바일 우선 UI 개선",
|
||||
-- "priority": "MEDIUM"
|
||||
-- }
|
||||
-- ]
|
||||
Loading…
x
Reference in New Issue
Block a user