diff --git a/design/uiux/uiux.md b/design/uiux/uiux.md
index db678ce..f4f16a9 100644
--- a/design/uiux/uiux.md
+++ b/design/uiux/uiux.md
@@ -628,12 +628,12 @@ graph TD
#### 개요
- **목적**: 실시간 회의 진행 및 AI 기반 회의록 자동 작성
-- **관련 유저스토리**: UFR-MEET-030, UFR-STT-010/020, UFR-AI-010, UFR-AI-040, UFR-COLLAB-010, UFR-RAG-010/020
+- **관련 유저스토리**: UFR-MEET-030, UFR-STT-010/020, UFR-AI-010, UFR-AI-040, UFR-COLLAB-010, UFR-RAG-010/020, UFR-PART-010/020/030, UFR-HOST-010/020, UFR-TERM-010/020
- **비즈니스 중요도**: 높음 (핵심 화면)
-- **접근 경로**: 템플릿선택 → "이 템플릿으로 시작"
-- **권한**:
- - 회의 시작/종료: 회의 생성자 전용
- - 참석자 초대: 모든 참석자
+- **접근 경로**: 대시보드 → "참여하기" 버튼 (페이지 전환)
+- **권한** (MVP 개선):
+ - **회의 생성자 전용**: 회의 종료, 녹음 제어 (일시정지/재개/종료)
+ - **모든 참석자**: 회의 참여, AI 주요 내용 체크, 용어 확인, 관련 회의록 확인, 중도 퇴장
#### 주요 기능
1. 음성 녹음 및 실시간 텍스트 변환 (STT)
diff --git a/design/userstory.md b/design/userstory.md
index 12088f8..85daa61 100644
--- a/design/userstory.md
+++ b/design/userstory.md
@@ -1,6 +1,6 @@
-# AI기반 회의록 작성 및 이력 관리 개선 서비스 - 유저스토리 (v2.3.0)
+# AI기반 회의록 작성 및 이력 관리 개선 서비스 - 유저스토리 (v2.4.0)
-- [AI기반 회의록 작성 및 이력 관리 개선 서비스 - 유저스토리 (v2.3.0)](#ai기반-회의록-작성-및-이력-관리-개선-서비스---유저스토리-v230)
+- [AI기반 회의록 작성 및 이력 관리 개선 서비스 - 유저스토리 (v2.4.0)](#ai기반-회의록-작성-및-이력-관리-개선-서비스---유저스토리-v240)
- [차별화 전략](#차별화-전략)
- [1. 기본 기능 (Hygiene Factors)](#1-기본-기능-hygiene-factors)
- [2. 핵심 차별화 포인트 (Differentiators)](#2-핵심-차별화-포인트-differentiators)
@@ -19,7 +19,6 @@
### 2. 핵심 차별화 포인트 (Differentiators)
- **맥락 기반 용어 설명**: 단순 용어 설명을 넘어, 관련 회의록과 업무이력을 바탕으로 실용적인 정보 제공
-- **강화된 Todo 연결**: Action item이 담당자의 Todo와 실시간으로 연결되고, 진행 상황이 회의록에 자동 반영
- **섹션 AI 요약 재생성**: 버튼 클릭으로 작성한 섹션 내용을 AI가 요약 (2-3문장, 2-5초 처리)
- **지능형 회의 진행 지원**: 회의 패턴 분석을 통한 안건 추천, 효율성 분석 및 개선 제안
@@ -31,7 +30,6 @@
2. **Meeting** - 회의, 회의록, Todo, 실시간 협업 통합 관리
- 회의 관리: 회의 예약, 시작, 종료
- 회의록 관리: 회의록 생성, 수정, 확정
- - Todo 관리: Todo 할당, 진행 상황 추적, 회의록 양방향 연결
- 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결
- 템플릿 관리: 회의록 템플릿 관리
- 통계 생성: 회의 및 Todo 통계
@@ -83,16 +81,15 @@
---
-### UFR-USER-020: [대시보드] 사용자로서 | 나는, 나의 회의 및 Todo 현황을 파악하기 위해 | 대시보드를 조회하고 싶다.
+### UFR-USER-020: [대시보드] 사용자로서 | 나는, 나의 회의 현황을 파악하기 위해 | 대시보드를 조회하고 싶다.
**수행절차:**
1. 로그인 후 대시보드 자동 표시 또는 하단 네비게이션에서 홈 아이콘 클릭
-2. 통계 블록 확인 (예정된 회의, 나의 Todo)
+2. 통계 블록 확인 (예정된 회의, 작성중 회의록)
3. 최근 회의 목록 확인 (최대 3개)
-4. 나의 Todo 목록 확인 (최대 3개)
-5. 나의 회의록 목록 확인 (최대 4개)
-6. 필요 시 섹션별 "전체 보기" 링크 클릭하여 상세 화면 이동
-7. FAB 버튼으로 회의 예약 또는 바로 시작
+4. 나의 회의록 목록 확인 (최대 4개)
+5. 필요 시 "전체 보기" 링크 클릭하여 회의록 목록 화면 이동
+6. FAB 버튼으로 회의 예약 또는 바로 시작
**입력:**
- 없음 (자동 렌더링)
@@ -101,18 +98,16 @@
- 헤더: 사용자 이름, 오늘 예정된 회의 개수 또는 안내 메시지
- 통계 블록 (2열 그리드):
- 예정된 회의: 전체 예정 + 진행 중 회의 개수
- - 나의 Todo: 현재 사용자에게 할당된 전체 Todo 개수
+ - 작성중 회의록: 내가 참석한 회의 중 '작성중' 상태인 회의록 개수 (클릭 액션 없음, 정보 표시만)
- 최근 회의 (회의록 미생성 우선, 빠른 일시 순, 최대 3개):
- 회의 카드: 상태 배지, 제목, 생성자 표시(👑), 날짜/시간, 참석자 수, 장소, 액션 버튼
- 진행 중 회의: 좌측 녹색 바 강조, "참여하기" 버튼
- 예정 회의: 생성자인 경우 "수정" 버튼
- 완료 회의: "보기" 버튼
-- 나의 Todo (미완료 우선, 마감일 순, 최대 3개):
- - Todo 카드: 체크박스, D-day 배지, 우선순위 배지, 제목, 회의록 링크, 마감일, 편집 버튼
- 나의 회의록 (최신순, 최대 4개, 2x2 그리드):
- 회의록 카드: 상태 배지, 생성자 표시(👑), 제목, 날짜/시간, 참석자 수, 검증완료율(작성중인 경우)
-- 사이드바 (데스크톱): 로고, 메뉴(회의록, Todo 관리), 사용자 정보, 로그아웃
-- 하단 네비게이션 (모바일): 홈(활성), 회의록, Todo
+- 사이드바 (데스크톱): 로고, 메뉴(대시보드, 회의록), 사용자 정보, 로그아웃
+- 하단 네비게이션 (모바일): 홈(활성), 회의록
- FAB 메뉴:
- 회의예약 버튼: 회의 예약 화면으로 이동
- 바로시작 버튼: 템플릿 선택 화면으로 이동
@@ -120,7 +115,6 @@
**예외처리:**
- 로그인 안 된 경우: 로그인 화면으로 리다이렉트
- 최근 회의 없음: 빈 상태 표시
-- Todo 없음: "할당된 Todo가 없습니다" 빈 상태 표시
- 회의록 없음: "참여한 회의록이 없습니다" 빈 상태 표시
**관련 유저스토리:**
@@ -129,7 +123,6 @@
- UFR-MEET-020: 템플릿선택
- UFR-MEET-046: 회의록목록조회
- UFR-MEET-047: 회의록상세조회
-- UFR-TODO-040: Todo관리
---
@@ -255,9 +248,13 @@
### UFR-MEET-030: [회의시작] 회의 생성자로서 | 나는, 회의를 시작하고 회의록을 작성하기 위해 | 회의를 시작하고 음성 녹음을 준비하고 싶다.
+**회의 진입 경로:**
+- **경로 1**: 대시보드(02-대시보드.html) → "바로시작" FAB 버튼 → 템플릿 선택(04-템플릿선택.html) → 회의 진행(05-회의진행.html)
+- **경로 2**: 대시보드 → 진행 중 회의 "참여하기" 버튼 → 회의 진행(05-회의진행.html)
+
**수행절차:**
-1. 템플릿 선택 완료 또는 대시보드에서 진행 중 회의 "참여하기" 클릭
-2. 회의 진행 화면(05-회의진행.html) 표시
+1. 위 경로 중 하나를 통해 회의 진행 화면(05-회의진행.html) 진입
+2. 회의 진행 화면 표시
3. 녹음 시작 확인 (자동 또는 수동)
4. 타이머 시작 (회의 진행 시간 표시)
5. 웨이브폼 애니메이션 표시 (녹음 상태 시각화)
@@ -302,8 +299,12 @@
### UFR-MEET-040: [회의종료] 회의 생성자로서 | 나는, 회의를 종료하고 회의록을 정리하기 위해 | 회의를 종료하고 요약 내용을 확인한 후 다음 단계를 선택하고 싶다.
+**권한:**
+- 회의 생성자만 회의를 종료할 수 있음
+- 일반 참석자는 "회의 종료" 버튼이 표시되지 않음
+
**수행절차:**
-1. 회의 진행 화면에서 "회의 종료" 버튼 클릭
+1. 회의 진행 화면에서 "회의 종료" 버튼 클릭 (생성자 전용)
2. 종료 확인 모달: "회의를 종료하시겠습니까?" 확인
3. 회의 종료 화면(07-회의종료.html) 표시 (확인 전용, 편집 불가)
4. 통계 확인 (4열 그리드):
@@ -450,8 +451,9 @@
- 주요 키워드 (태그)
- 핵심내용 요약
- 결정사항
- - Todo 진행상황 (필터: 전체, 지연, 마감 임박, 완료)
- - Todo 카드: 체크박스, D-day 배지, 우선순위 배지, 제목, 담당자, 마감일, 편집 버튼
+ - Todo 진행상황 (단순 조회 전용)
+ - Todo 카드: 제목, 담당자, 마감일만 표시
+ - D-day 체크, 우선순위 라벨 제거
- 진행률 표시 (수평 바): 완료 Todo 수 / 전체 Todo 수
- 관련회의록 (관련도 배지: 높음, 중간, 낮음)
- **회의록 탭**:
@@ -461,103 +463,102 @@
- AI 한줄 요약 (30자 이내, 읽기 전용)
- AI 상세 요약
- 관련회의록 링크
-4. Todo 체크박스 클릭 시 완료/미완료 토글
-5. Todo 편집 버튼 클릭 시 편집 모달
-6. "수정" 버튼 클릭 시 회의록 수정 화면으로 이동
+4. "수정" 버튼 클릭 시 회의록 수정 화면으로 이동
**입력:**
- 탭 클릭: 대시보드 / 회의록
-- Todo 필터: 전체, 지연, 마감 임박, 완료
-- Todo 체크박스: 완료/미완료 토글
-- Todo 편집 버튼: 편집 모달 열기
**출력/결과:**
-- 헤더: 회의 제목, 생성자 표시 (👑), "수정" 버튼 (권한에 따라 표시)
+- 헤더: 회의 제목, "수정" 버튼 (제목 텍스트 바로 옆에 배치, 권한에 따라 표시)
- 대시보드 탭:
- 통계, 키워드, 핵심내용, 결정사항, Todo 진행상황, 관련회의록
+ - Todo 카드: 제목, 담당자, 마감일만 표시 (D-day, 우선순위 라벨 없음)
- 회의록 탭:
- 기본 정보, 안건별 상세 내용
-- Todo 완료 토글: 확인 모달 → 상태 변경 → "Todo가 완료되었습니다" 토스트
-- Todo 편집 모달: 제목, 담당자, 마감일, 우선순위 수정
**예외처리:**
- 회의록 조회 실패: "회의록 조회 중 오류가 발생했습니다" 에러 메시지
- Todo 없음: "등록된 Todo가 없습니다" 빈 상태 표시
- 관련회의록 없음: "관련 회의록이 없습니다" 빈 상태 표시
-- Todo 완료 토글 실패: "Todo 상태 변경 중 오류가 발생했습니다" 에러 메시지
-- Todo 편집 권한 없음: "담당자 또는 회의 생성자만 편집할 수 있습니다" 안내
**관련 유저스토리:**
- UFR-MEET-046: 회의록목록조회
- UFR-MEET-055: 회의록수정
-- UFR-TODO-030: Todo완료처리
-- UFR-TODO-040: Todo관리
---
-### UFR-MEET-055: [회의록수정] 회의 참석자로서 | 나는, 검증이 완료되지 않았거나 수정이 필요한 | 지난 회의록을 수정하고 싶다.
+### UFR-MEET-055: [회의록수정] 회의 참석자로서 | 나는, 검증이 완료되지 않은 안건을 | 수정하고 검증완료 체크를 통해 보호하고 싶다.
**수행절차:**
1. 회의록 상세 조회 화면에서 "수정" 버튼 클릭
2. 회의록 수정 화면(11-회의록수정.html) 표시
-3. 편집 가능 항목:
+3. 여러 참석자가 동시에 수정 화면 진입 가능 (동시 편집 허용)
+4. 편집 가능 항목:
- 회의 제목 (텍스트 입력)
- AI 한줄 요약 (읽기 전용, 편집 불가)
- - AI 상세 요약 (텍스트 영역, 편집 가능)
- - "재생성" 버튼: AI 상세 요약 재생성 (2-3문장, 2-5초 처리)
- - 안건 내용 (텍스트 영역)
+ - 안건별 입력창 내용 (텍스트 영역, 편집 가능)
+ - "AI 재생성" 버튼: 입력창 내용 기반으로 한줄 요약만 재생성
- 관련회의록 추가/제거
- "추가" 버튼: 검색 모달 열기
- X 버튼: 관련회의록 제거
- 참석자 관리 (회의 생성자만):
- 참석자 추가/삭제
- 05-회의진행 화면과 동일한 UI
-4. 읽기 전용 항목:
+5. 읽기 전용 항목:
- 회의 일시 (읽기 전용 배지 표시)
- 회의 장소 (읽기 전용 배지 표시)
-5. 안건별 검증 완료 토글:
- - 검증 완료 전: 토글 활성화 → 잠금
- - 검증 완료 후: 잠금 아이콘 표시
- - 회의 생성자: 잠금 해제 → 토글 비활성화 → 편집 가능
-6. 자동 저장 (30초마다)
- - 저장 상태 표시: "저장 중...", "저장됨"
-7. 모든 안건 검증 완료 시 "최종 확정" 버튼 활성화
-8. 페이지 나가기 전 확인 (변경사항 있을 때)
+6. 안건별 검증완료 체크:
+ - 검증완료 전: 체크박스 비활성화 상태, 편집 가능
+ - 검증완료 체크 시: 해당 안건 잠금 (편집 불가)
+ - 회의 생성자만: 검증완료 체크 해제 가능 (잠금 해제)
+7. 저장 로직:
+ - "저장" 버튼 클릭 시:
+ - 검증완료된 안건: 저장 스킵
+ - 미검증 안건: 저장 진행
+ - 저장 결과 알림:
+ - "N개 안건이 저장되었습니다"
+ - "M개 안건은 검증완료 상태로 저장되지 않았습니다"
+ - 저장 불가 안건 목록 표시
+8. 모든 안건 검증 완료 시 "최종 확정" 버튼 활성화
+9. 페이지 나가기 전 확인 (변경사항 있을 때)
**입력:**
- 회의 제목: 텍스트 입력
-- AI 상세 요약: 텍스트 영역 편집, "재생성" 버튼
-- 안건 내용: 텍스트 영역 편집
+- 안건별 입력창 내용: 텍스트 영역 편집
+- AI 재생성: 버튼 클릭 (입력창 내용 기반 한줄 요약 생성)
- 관련회의록: 추가(검색 모달), 제거(X 버튼)
- 참석자: 추가(검색 모달), 삭제(X 버튼) - 회의 생성자만
-- 검증 완료: 토글 스위치
-- 잠금 해제: 잠금 아이콘 클릭 (회의 생성자만)
+- 검증 완료: 체크박스 (회의 생성자: 체크/해제, 참석자: 체크만)
**출력/결과:**
-- 편집 화면: 제목, AI 요약, 안건, 관련회의록, 참석자, 검증 토글
-- 자동 저장: "저장 중..." → "저장됨" 상태 표시
-- AI 재생성: "재생성 중..." → 새 요약 표시 → "재생성되었습니다" 토스트
+- 편집 화면: 제목, AI 한줄 요약, 안건별 입력창, 관련회의록, 참석자, 검증 체크박스
+- 저장 결과: "N개 안건 저장됨" 토스트, 저장 불가 안건 목록 표시
+- AI 재생성: "재생성 중..." → 한줄 요약만 업데이트 → "재생성되었습니다" 토스트
- 관련회의록 추가: 검색 모달 → 선택 → "추가되었습니다" 토스트
- 참석자 추가: 검색 모달 → 선택 → "{이름}님이 추가되었습니다" 토스트
-- 검증 완료: 토글 → 잠금 아이콘 표시
-- 잠금 해제: 잠금 아이콘 클릭 → "잠금 해제하시겠습니까?" 확인 → 토글 비활성화
+- 검증 완료: 체크 → 잠금 아이콘 표시 (편집 불가)
+- 잠금 해제 (회의 생성자만): 체크 해제 → "잠금 해제하시겠습니까?" 확인 → 편집 가능
- 최종 확정: 검증률 100% → "최종 확정" 버튼 활성화
**예외처리:**
-- 편집 권한 없음 (확정 완료 후, 생성자 아님): 읽기 전용 모드, "회의 생성자만 수정할 수 있습니다" 안내
-- 검증 완료 섹션 편집 시도 (참석자): "검증 완료된 섹션은 수정할 수 없습니다" 안내
+- 편집 권한 없음 (확정 완료 후): 읽기 전용 모드, "확정된 회의록은 수정할 수 없습니다" 안내
+- 검증완료 안건 편집 시도: "검증완료된 안건은 수정할 수 없습니다. 회의 생성자에게 문의하세요" 안내
+- 검증완료 해제 권한 없음: "회의 생성자만 검증완료를 해제할 수 있습니다" 안내
- AI 재생성 실패: "재생성 중 오류가 발생했습니다" 에러 메시지
-- 자동 저장 실패: "저장 실패" 상태 표시
+- 저장 실패: "저장 중 오류가 발생했습니다" 에러 메시지
- 페이지 나가기: "저장되지 않은 변경사항이 있습니다. 나가시겠습니까?" 확인 모달
- 참석자 관리 권한 없음: "회의 생성자만 참석자를 관리할 수 있습니다" 안내
+**동시 편집 정책 (MVP):**
+- 실시간 동기화 없음 (다른 참석자가 편집하는 내용 안 보임)
+- 안건별 검증완료 체크로 충돌 방지
+- 마지막 저장 우선 원칙 (Last Write Wins)
+
**관련 유저스토리:**
- UFR-MEET-047: 회의록상세조회
- UFR-MEET-050: 최종확정
-- UFR-AI-035: 섹션AI요약
+- UFR-AI-036: AI한줄요약
- UFR-AI-040: 관련회의록연결
-- UFR-COLLAB-010: 회의록수정동기화
-- UFR-COLLAB-020: 충돌해결
- UFR-COLLAB-030: 검증완료
# 유저스토리 v2.3.0 - AI, STT, RAG, COLLAB, TODO, NOTIFICATION 서비스
@@ -742,19 +743,29 @@
---
-### UFR-AI-040: [관련회의록연결] 회의 참석자로서 | 나는, 이전 회의 내용을 쉽게 참조하기 위해 | AI가 같은 폴더 내 관련 있는 과거 회의록을 자동으로 찾아 연결해주기를 원한다.
+### UFR-AI-040: [관련회의록연결] 회의 참석자로서 | 나는, 이전 회의 내용을 쉽게 참조하기 위해 | AI가 같은 폴더 내 관련 있는 과거 회의록을 자동으로 찾아 연결하고 유사 내용을 요약해주기를 원한다.
+
+**기능 개선 사항 (v2.3.1):**
+- **최대 개수**: 5개 → 3개로 축소 (MVP)
+- **관련도 표시 방식**: 배지(높음/중간/낮음) → 퍼센트(95%, 78%)로 변경
+- **유사 내용 요약 추가** (신규):
+ - AI가 추천한 각 회의록에서 현재 회의와 유사한 부분 자동 추출
+ - 유사한 내용을 3-5개 문장으로 요약하여 표시
+ - 전체 회의록을 열지 않아도 핵심 내용 파악 가능
+ - "전체 회의록 보기" 버튼으로 상세 내용 확인
+- **성능 최적화**:
+ - 과거 회의록 저장 시 요약본 미리 생성 (배치 처리)
+ - 실시간 요약은 캐싱된 데이터 활용
+ - 성능 목표: 1초 이내 표시
**수행절차:**
1. 회의 종료 시 또는 회의록 작성 중 AI가 현재 회의 내용 분석
2. 벡터 유사도 검색을 통해 관련 회의록 탐색
- 같은 폴더 내 회의록 우선
- 키워드, 참석자, 안건 유사도 분석
-3. 관련도가 높은 회의록 자동 연결 (최대 5개)
-4. 관련도 배지 부여:
- - 높음 (80% 이상)
- - 중간 (60-80%)
- - 낮음 (40-60%)
-5. 회의 종료 화면, 회의록 수정 화면, 회의록 상세 조회 화면에 표시
+3. 관련도가 높은 회의록 자동 연결 (최대 3개로 축소)
+4. 각 회의록에서 유사한 내용 추출 및 요약 (신규 추가)
+5. 회의 진행 화면, 회의 종료 화면, 회의록 수정 화면, 회의록 상세 조회 화면에 표시
6. 사용자가 수동으로 추가/제거 가능
**입력:**
@@ -762,10 +773,12 @@
- 과거 회의록 데이터 (벡터 DB)
**출력/결과:**
-- 관련 회의록 목록 (최대 5개):
+- 관련 회의록 목록 (최대 3개):
- 회의록 제목
- 날짜
- - 관련도 배지 (높음/중간/낮음)
+ - 관련도 점수 (퍼센트, 예: 95%, 78%)
+ - **유사 내용 요약 (3-5개 문장)** ← 신규
+ - "전체 회의록 보기" 버튼
- 회의록 링크
- 회의록 수정 화면: "추가" 버튼, "X" 제거 버튼
- 회의록 상세 조회 화면: 관련회의록 섹션 표시
@@ -773,6 +786,7 @@
**예외처리:**
- 관련 회의록 없음: 빈 목록 표시
- 벡터 검색 실패: 최근 회의록 기준 정렬로 대체
+- 유사 내용 요약 실패: 회의록 제목과 날짜만 표시
- 네트워크 오류: 로컬 캐시 사용 또는 빈 목록
**관련 유저스토리:**
@@ -1070,149 +1084,6 @@
---
-## TODO 서비스 (Meeting 서비스에 통합)
-
-### UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Todo를 담당자에게 전달하기 위해 | Todo를 실시간으로 할당하고 회의록과 연결하고 싶다.
-
-**수행절차:**
-1. AI 서비스에서 Todo 추출 완료 (UFR-AI-020)
-2. Todo 데이터 수신:
- - 제목, 담당자 ID, 마감일, 우선순위
- - 회의 ID, 안건 ID
-3. Todo 데이터베이스에 저장
-4. 담당자에게 알림 대상 생성:
- - 알림 유형: "Todo 할당"
- - 알림 내용: "{회의 제목}에서 Todo가 할당되었습니다: {Todo 제목}"
- - 수신자: 담당자
- - 발송 예정 시각: 즉시
-5. Notification 서비스가 주기적으로 폴링하여 이메일 발송
-6. 회의록과 Todo 양방향 연결:
- - 회의록 → Todo 링크
- - Todo → 회의록 링크
-7. 담당자의 Todo 목록 및 대시보드에 표시
-
-**입력:**
-- AI가 추출한 Todo 데이터:
- - 제목, 담당자, 마감일, 우선순위
-- 회의 정보: 회의 ID, 안건 ID, 회의 제목
-
-**출력/결과:**
-- Todo 저장 완료
-- 알림 대상 생성 (Notification 테이블에 레코드 추가)
-- 회의록과 Todo 양방향 링크 생성
-- 담당자 Todo 목록 업데이트
-- 회의록 화면에 Todo 표시
-
-**예외처리:**
-- 담당자 정보 없음: 회의록 생성자로 할당
-- Todo 저장 실패: "Todo 저장 중 오류가 발생했습니다" 로그 기록, 재시도
-- 알림 대상 생성 실패: 로그 기록, Todo는 정상 저장
-
-**관련 유저스토리:**
-- UFR-AI-020: Todo자동추출
-- UFR-TODO-040: Todo관리
-- UFR-NOTI-010: 알림발송 (신규)
-
----
-
-### UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo를 처리하고 회의록에 반영하기 위해 | Todo를 완료하고 회의록에 자동 반영하고 싶다.
-
-**수행절차:**
-1. Todo 관리 화면 또는 회의록 상세 조회 화면에서 Todo 체크박스 클릭
-2. 완료 확인 모달: "완료 처리하시겠습니까?"
-3. 승인 시 Todo 상태 "완료"로 변경
-4. 완료 시각 기록
-5. 회의록에 자동 반영:
- - 회의록 상세 조회 시 완료된 Todo 표시
- - Todo 진행상황 진행률 업데이트
-6. 알림 대상 생성 (옵션):
- - 회의 생성자에게 Todo 완료 알림
- - 알림 내용: "{담당자}님이 Todo를 완료했습니다: {Todo 제목}"
-7. "Todo가 완료되었습니다" 토스트 메시지
-
-**미완료로 되돌리기:**
-1. 완료된 Todo 체크박스 클릭
-2. 확인 모달: "미완료로 변경하시겠습니까?"
-3. 승인 시 Todo 상태 "미완료"로 변경
-4. 회의록 진행률 자동 업데이트
-
-**입력:**
-- Todo 체크박스 클릭 (완료/미완료 토글)
-- 확인 모달 승인
-
-**출력/결과:**
-- Todo 상태 변경: 완료 ↔ 미완료
-- 완료 시각 기록
-- 회의록 진행률 업데이트
-- 알림 대상 생성 (옵션)
-- 토스트 메시지: "Todo가 완료되었습니다" / "미완료로 변경되었습니다"
-
-**예외처리:**
-- 네트워크 오류: "Todo 상태 변경 중 오류가 발생했습니다" 에러 메시지
-- 확인 모달 취소: 체크박스 상태 원복
-
-**관련 유저스토리:**
-- UFR-TODO-010: Todo할당
-- UFR-TODO-040: Todo관리
-- UFR-MEET-047: 회의록상세조회
-- UFR-NOTI-010: 알림발송 (신규)
-
----
-
-### UFR-TODO-040: [Todo관리] Todo 담당자로서 | 나는, 나의 Todo를 효율적으로 관리하기 위해 | Todo 목록을 조회하고 상태를 변경하고 편집하고 싶다.
-
-**수행절차:**
-1. 사이드바 또는 하단 네비게이션에서 "Todo 관리" 메뉴 클릭
-2. Todo 관리 화면(09-Todo관리.html) 표시
-3. 통계 블록 확인 (3열 그리드):
- - 전체 (미완료): 완료되지 않은 전체 Todo 개수
- - 마감 임박 (3일 이내): D-3 이내 미완료 Todo
- - 지연 (기한 경과): 마감일이 지난 미완료 Todo
-4. 필터 탭 선택:
- - 전체: 모든 Todo (미완료 우선, 마감일 순)
- - 지연: 마감일 지난 Todo
- - 마감 임박: D-3 이내 Todo
- - 완료: 완료된 Todo
-5. Todo 카드 확인:
- - 체크박스, D-day 배지, 우선순위 배지
- - 제목, 회의록 링크, 마감일
- - 편집 버튼 (미완료 Todo만)
-6. 체크박스 클릭 시 완료/미완료 토글 (확인 모달)
-7. 편집 버튼 클릭 시 편집 모달:
- - 제목, 담당자, 마감일, 우선순위 수정
- - 권한: 담당자 본인 OR 회의 생성자
-8. Todo 카드 클릭 시 해당 회의록 상세 조회 화면으로 이동
-
-**입력:**
-- 필터 탭 클릭
-- Todo 체크박스 클릭
-- 편집 버튼 클릭
-- Todo 카드 클릭
-
-**출력/결과:**
-- 통계 블록 (3열):
- - 전체 (미완료), 마감 임박, 지연 개수
-- 필터 탭: 전체, 지연, 마감 임박, 완료 (각 탭에 개수 표시)
-- Todo 카드:
- - D-day 배지 (완료, D+n(지연), D-DAY, D-n)
- - 우선순위 배지 (높음, 보통, 낮음)
- - 제목, 회의록 링크, 마감일, 편집 버튼
-- 편집 모달: 제목, 담당자, 마감일, 우선순위 입력 필드
-- 빈 상태: "할당된 Todo가 없습니다"
-
-**예외처리:**
-- Todo 없음: 빈 상태 표시
-- 편집 권한 없음: "담당자 또는 회의 생성자만 편집할 수 있습니다" 안내
-- 편집 실패: "Todo 수정 중 오류가 발생했습니다" 에러 메시지
-- 네트워크 오류: "Todo 조회 중 오류가 발생했습니다" 에러 메시지
-
-**관련 유저스토리:**
-- UFR-TODO-010: Todo할당
-- UFR-TODO-030: Todo완료처리
-- UFR-USER-020: 대시보드 조회
-
----
-
## NOTIFICATION 서비스 (신규)
### UFR-NOTI-010: [알림발송] Notification 시스템으로서 | 나는, 사용자에게 중요한 이벤트를 알리기 위해 | 주기적으로 알림 대상을 확인하여 이메일을 발송하고 싶다.
@@ -1233,8 +1104,6 @@
- 발송 실패 시 재시도 횟수 증가, 최대 3회 재시도
4. **알림 유형 및 트리거**:
- - **Todo 할당**: AI가 Todo 추출 후 담당자에게 알림
- - **Todo 완료**: 담당자가 Todo 완료 시 회의 생성자에게 알림 (옵션)
- **회의 시작 알림**: 회의 시작 10분 전 참석자에게 알림
- **회의록 확정**: 회의록 최종 확정 시 모든 참석자에게 알림
- **참석자 초대**: 회의 중 참석자 초대 시 신규 참석자에게 알림
@@ -1261,10 +1130,6 @@
**알림 유형별 템플릿 예시:**
-**Todo 할당**
-- 제목: "[회의록] Todo가 할당되었습니다"
-- 내용: "{담당자}님, {회의 제목} 회의에서 다음 Todo가 할당되었습니다:\n- {Todo 제목}\n- 마감일: {마감일}\n\n회의록 보기: {회의록 링크}"
-
**회의 시작 알림**
- 제목: "[회의록] 10분 후 회의가 시작됩니다"
- 내용: "{참석자}님, {회의 제목} 회의가 10분 후 시작됩니다.\n- 일시: {날짜} {시간}\n- 장소: {장소}\n\n회의 참여하기: {회의 링크}"
@@ -1281,9 +1146,256 @@
---
+# MVP 개선 사항 (v2.3.1)
+
+## 회의 참석자 권한 및 기능 단순화
+
+### UFR-PART-010: [회의입장] 회의 참석자로서 | 나는, 예정된 회의에 참여하기 위해 | 대시보드에서 "참여하기" 버튼으로 회의에 입장하고 싶다.
+
+**수행절차:**
+1. 대시보드에서 진행 중 또는 예정된 회의 확인
+2. "참여하기" 버튼 클릭
+3. 회의진행 화면으로 페이지 전환 (같은 탭)
+4. 회의 참석자로 입장 완료
+
+**입력:**
+- 회의 ID
+- 사용자 ID
+
+**출력/결과:**
+- 회의진행 화면 표시
+- 참석자 목록에 추가
+
+**예외처리:**
+- 회의 종료됨: "이미 종료된 회의입니다" 안내
+- 권한 없음: "참석 권한이 없습니다" 안내
+
+**관련 유저스토리:**
+- UFR-MEET-030: 회의시작
+
+---
+
+### UFR-PART-020: [AI기반메모작성] 회의 참석자로서 | 나는, 중요한 내용을 메모로 기록하기 위해 | AI가 추천한 주요 내용을 메모 입력창에 추가하고 편집하고 싶다.
+
+**수행절차:**
+1. 회의 진행 중 AI가 실시간으로 주요 내용 감지 및 분석
+2. "AI 제안" 탭 하단에 "AI가 감지한 주요 내용" 리스트로 표시
+ - 각 항목: "[시간] 주요 내용 텍스트" 형식
+ - 예: "[15:32] 예산 책정 관련 결정", "[15:35] 다음 회의 일정 합의"
+3. 참석자가 리스트 항목 선택 시:
+ - 메모 입력창에 시간과 함께 자동 입력
+ - 입력된 메모는 수정 가능 (자동/수동 구분 표시)
+4. "저장" 버튼 클릭 시 개인 메모로 저장
+ - 각 참석자별로 개별 저장
+ - 다른 참석자의 메모는 볼 수 없음
+5. 회의 종료 시 AI가 회의록 생성할 때 모든 참석자의 메모 참조
+6. 메모는 회의 종료 전까지만 표시 및 편집 가능
+
+**입력:**
+- 회의 ID
+- AI가 실시간 감지한 주요 내용
+- 참석자가 선택하거나 직접 입력한 메모 텍스트
+
+**출력/결과:**
+- 메모 입력창: 시간 포함 메모 텍스트
+- AI 추천 리스트: 실시간 업데이트되는 주요 내용 항목들
+- 저장된 개인 메모: 참석자별 개별 저장
+- AI 회의록 생성 시: 모든 참석자의 메모 참조하여 요약 생성
+
+**예외처리:**
+- AI 감지 실패: 빈 리스트 표시, 수동 메모 작성은 가능
+- 저장 실패: "메모 저장 중 오류가 발생했습니다" 에러 메시지, 로컬 임시 저장
+- 회의 종료 후: 메모 조회/편집 불가
+
+**관련 유저스토리:**
+- UFR-AI-030: 실시간 AI 제안
+- UFR-AI-010: 회의록자동작성 (메모 참조)
+- UFR-MEET-040: 회의종료
+
+---
+
+### UFR-PART-030: [회의중도퇴장] 회의 참석자로서 | 나는, 회의를 중간에 나가야 할 때 | "나가기" 버튼으로 회의에서 퇴장하고 싶다.
+
+**수행절차:**
+1. 회의진행 화면 상단 "나가기" 버튼 클릭
+2. 확인 모달 표시: "회의에서 나가시겠습니까? 회의는 계속 진행됩니다"
+3. 확인 클릭 시 퇴장 이벤트 서버 전송
+4. 대시보드로 페이지 전환
+
+**입력:**
+- 회의 ID
+- 사용자 ID
+
+**출력/결과:**
+- 대시보드 화면으로 복귀
+- 회의록은 종료 시 공유됨
+
+**예외처리:**
+- 네트워크 오류: 로컬 처리 후 재시도
+
+**관련 유저스토리:**
+- UFR-MEET-030: 회의시작
+
+---
+
+## 회의 생성자 전용 기능
+
+### UFR-HOST-010: [회의종료권한] 회의 생성자로서 | 나는, 회의를 마무리하기 위해 | "회의 종료" 버튼으로만 회의를 종료하고 싶다.
+
+**수행절차:**
+1. 회의진행 화면에서 "회의 종료" 버튼은 생성자에게만 표시
+2. 일반 참석자에게는 버튼 숨김 처리
+3. 생성자가 "회의 종료" 클릭 시 회의 종료 프로세스 진행
+
+**입력:**
+- 회의 ID
+- 사용자 ID (생성자 여부 확인)
+
+**출력/결과:**
+- 생성자: 회의 종료 버튼 표시 및 동작
+- 일반 참석자: 버튼 숨김
+
+**예외처리:**
+- 권한 없음: 버튼 미표시
+
+**관련 유저스토리:**
+- UFR-MEET-040: 회의종료
+
+---
+
+### UFR-HOST-020: [녹음제어권한] 회의 생성자로서 | 나는, 녹음을 관리하기 위해 | 녹음 일시정지/재개/종료 권한을 가지고 싶다.
+
+**수행절차:**
+1. 회의진행 화면에서 녹음 제어 버튼은 생성자에게만 표시
+2. 일반 참석자에게는 버튼 숨김 처리
+
+**입력:**
+- 회의 ID
+- 사용자 ID (생성자 여부 확인)
+
+**출력/결과:**
+- 생성자: 녹음 제어 버튼 표시 및 동작
+- 일반 참석자: 버튼 숨김
+
+**예외처리:**
+- 권한 없음: 버튼 미표시
+
+**관련 유저스토리:**
+- UFR-STT-010: 음성녹음인식
+
+---
+
+## 용어 설명 기능 (MVP 단순화)
+
+### UFR-TERM-010: [용어자동감지] 회의 참석자로서 | 나는, 전문용어를 이해하기 위해 | AI가 자동으로 감지한 용어를 "용어" 탭에서 확인하고 싶다.
+
+**수행절차:**
+1. AI가 STT 분석 중 중요 용어 자동 감지
+2. "용어" 탭에 실시간으로 표시
+3. 용어 항목 클릭 시 상세 설명 모달 표시
+
+**입력:**
+- STT 텍스트
+- 회사 용어 사전 (JSON)
+
+**출력/결과:**
+- 용어 목록 표시
+- 회사 용어는 ⭐ 배지 표시
+
+**예외처리:**
+- 용어 감지 실패: 빈 목록 표시
+
+**관련 유저스토리:**
+- UFR-RAG-010: 전문용어감지 (기존 기능 유지)
+
+---
+
+### UFR-TERM-020: [회사용어사전] 회의 참석자로서 | 나는, 회사 특화 용어를 정확히 이해하기 위해 | 회사 용어 사전에 등록된 용어를 우선 표시받고 싶다.
+
+**수행절차:**
+1. 용어 감지 시 회사 용어 사전(JSON) 먼저 확인
+2. 사전에 있으면 회사 특화 설명 + ⭐ 표시
+3. 사전에 없으면 AI가 일반 설명 + 회의 맥락 제공
+
+**입력:**
+- 용어
+- 회사 용어 사전 (terms-dictionary.json)
+- 회의 맥락
+
+**출력/결과:**
+- 회사 용어: 정의 + 맥락 + ⭐
+- 일반 용어: AI 설명
+
+**예외처리:**
+- 사전 로드 실패: AI 설명으로 대체
+
+**관련 유저스토리:**
+- UFR-RAG-020: 맥락기반용어설명 (기존 기능 유지)
+
+---
+
+### UFR-TERM-030: [용어관리] 관리자로서 | 나는, 회사 특화 용어를 관리하기 위해 | 용어 사전을 등록/수정하고 싶다.
+
+**수행절차:**
+1. 관리자 페이지에서 용어 사전 관리
+2. JSON 파일 직접 편집 또는 관리 UI 사용
+3. 용어 추가/수정/삭제
+
+**입력:**
+- 용어명
+- 정의
+- 맥락
+- 카테고리
+
+**출력/결과:**
+- terms-dictionary.json 업데이트
+
+**예외처리:**
+- 권한 없음: "관리자 권한이 필요합니다" 안내
+
+---
+
+## 기존 유저스토리 수정
+
+### UFR-MEET-040 수정 (회의종료권한)
+**기존**: "회의 생성자 또는 참석자가 회의 종료 버튼을 눌러 회의를 종료한다"
+**변경**: "회의 생성자만 '회의 종료' 버튼으로 회의를 종료할 수 있다"
+**적용 위치**: [UFR-MEET-040: 회의종료](#ufr-meet-040-회의종료)
+
+---
+
+### UFR-MEET-030 개선 (회의 진입 경로)
+**변경**: 대시보드에서 회의 진행 화면 진입 경로 명확화
+- **경로 1**: 대시보드 → "바로시작" FAB 버튼 → 템플릿 선택 → 회의 진행
+- **경로 2**: 대시보드 → 진행 중 회의 "참여하기" 버튼 → 회의 진행
+**적용 위치**: [UFR-MEET-030: 회의시작](#ufr-meet-030-회의시작)
+
+---
+
+### UFR-AI-040 개선 (관련회의록연결)
+
+**변경 및 개선 사항**:
+1. **최대 개수**: 5개 → 3개로 축소 (MVP)
+2. **관련도 표시 방식**: 배지(높음/중간/낮음) → 퍼센트(95%, 78%)로 변경
+3. **유사 내용 요약 추가** (신규):
+ - AI가 추천한 각 회의록에서 현재 회의와 유사한 부분 자동 추출
+ - 유사한 내용을 3-5개 문장으로 요약하여 표시
+ - 전체 회의록을 열지 않아도 핵심 내용 파악 가능
+ - "전체 회의록 보기" 버튼으로 상세 내용 확인
+4. **성능 최적화**:
+ - 과거 회의록 저장 시 요약본 미리 생성 (배치 처리)
+ - 실시간 요약은 캐싱된 데이터 활용
+ - 성능 목표: 1초 이내 표시
+
+**적용 위치**: [UFR-AI-040: 관련회의록연결](#ufr-ai-040-관련회의록연결)
+
+---
+
## 문서 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|------|------|--------|-----------|
+| v2.4.0 | 2025-10-27 | 팀 전체 | • MVP 스코프 축소: Todo 관리 기능 제거
• UFR-USER-020 수정: 대시보드에서 "나의 Todo" 제거, "작성중 회의록" 추가
• UFR-PART-020 변경: AI주요내용체크 → AI기반메모작성 (메모 입력창 + AI 추천)
• UFR-AI-010 개선: 회의록 생성 시 참석자 메모 참조
• UFR-MEET-055 개선: 회의록 수정 시 실시간 협업 제거, 검증완료 체크로 보호
• TODO 서비스 전체 제거 (UFR-TODO-010/030/040)
• NOTIFICATION 서비스: Todo 관련 알림 제거
• 네비게이션 간소화: Todo 관리 메뉴 제거 (대시보드, 회의록만 유지) |
+| v2.3.1 | 2025-10-27 | 팀 전체 | • MVP 개선: 회의 참석자 권한 단순화
• 신규 유저스토리: UFR-PART-010/020/030 (참석자 공통), UFR-HOST-010/020 (생성자 전용)
• 신규 유저스토리: UFR-TERM-010/020/030 (용어 기능 MVP 단순화)
• UFR-MEET-040 수정: 회의 종료 권한 생성자 전용으로 명확화
• UFR-MEET-030 개선: 회의 진입 경로 2가지 명시 (바로시작, 참여하기)
• UFR-AI-040 개선: 관련 회의록 유사 내용 요약 추가 (최대 3개, 퍼센트 표시, 3-5문장 요약)
• 메모 기능 단순화: 체크박스 방식으로 변경
• 용어 설명 단순화: JSON 용어 사전 방식 도입 |
| v2.3.0 | 2025-10-24 | 이미준 | • 프로토타입 분석을 통한 유저스토리 전면 재정비
• 신규 유저스토리 추가: UFR-MEET-015 (참석자 실시간 초대), UFR-NOTI-010 (알림 발송)
• 알림 아키텍처 폴링 방식으로 통일 (실시간 발송 → 주기적 폴링)
• 10개 프로토타입 화면 반영 완료
• 마이크로서비스 구성 재정의 (User, Meeting, STT, AI, Notification)
• 기존 24개 유저스토리 ID 승계 및 정리 |
| v2.2.0 | 2025-10-23 | 이미준 | 이전 버전 |
diff --git a/meeting/README.md b/meeting/README.md
new file mode 100644
index 0000000..05763f9
--- /dev/null
+++ b/meeting/README.md
@@ -0,0 +1,25 @@
+# notification 메일 알림발송
+
+아래와 같이 메일발송 Email List를 loop돌려서 event 객체를 생성한 후에 `publishNotificationRequest` 메소드를 통해 Event 메세지 발행하시면 됩니다.
+
+```
+ // 각 참석자에게 개별 알림 이벤트 발행
+ for (String participantEmail : participants) {
+ NotificationRequestEvent event = NotificationRequestEvent.builder()
+ .notificationType("MEETING_INVITATION")
+ .recipientEmail(participantEmail)
+ .recipientId(participantEmail)
+ .recipientName(participantEmail)
+ .title("회의 초대")
+ .message(String.format("'%s' 회의에 초대되었습니다. 일시: %s, 장소: %s",
+ title, startTime, location))
+ .relatedEntityId(meetingId)
+ .relatedEntityType("MEETING")
+ .requestedBy(organizerId)
+ .requestedByName(organizerName)
+ .eventTime(LocalDateTime.now())
+ .build();
+
+ publishNotificationRequest(event);
+ }
+```
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting$MeetingBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting$MeetingBuilder.class
index 7520636..427963b 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting$MeetingBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting$MeetingBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting.class
index f098189..c9b6bed 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/domain/Meeting.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$MeetingDTOBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$MeetingDTOBuilder.class
index a954bea..936a3b6 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$MeetingDTOBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$MeetingDTOBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO$ParticipantDTOBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO$ParticipantDTOBuilder.class
index cc01cd9..bb2d916 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO$ParticipantDTOBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO$ParticipantDTOBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO.class
index d38c033..53760cb 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO$ParticipantDTO.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO.class
index 36aabba..c5174fe 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/dto/MeetingDTO.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/service/MeetingService.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/service/MeetingService.class
index 71dea6d..9b9a824 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/service/MeetingService.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/service/MeetingService.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/CreateMeetingUseCase$CreateMeetingCommand.class b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/CreateMeetingUseCase$CreateMeetingCommand.class
index 81ada94..0ddb7a2 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/CreateMeetingUseCase$CreateMeetingCommand.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/CreateMeetingUseCase$CreateMeetingCommand.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/config/SwaggerConfig.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/config/SwaggerConfig.class
index b26c87a..819cc9c 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/config/SwaggerConfig.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/config/SwaggerConfig.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/DashboardController.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/DashboardController.class
index 4d7ad2a..11957a1 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/DashboardController.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/DashboardController.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/MeetingController.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/MeetingController.class
index 9e996ed..1a0af43 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/MeetingController.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/MeetingController.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/TemplateController.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/TemplateController.class
index bf8c134..9da6101 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/TemplateController.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/controller/TemplateController.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$MeetingResponseBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$MeetingResponseBuilder.class
index 69106d2..9320e91 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$MeetingResponseBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$MeetingResponseBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse$ParticipantResponseBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse$ParticipantResponseBuilder.class
index fc2b9ed..e1e1f03 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse$ParticipantResponseBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse$ParticipantResponseBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse.class
index 26339f7..26e325f 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse$ParticipantResponse.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse.class
index 2ef056f..9255882 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/MeetingResponse.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem$TemplateItemBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem$TemplateItemBuilder.class
index 3aebc7f..575b9e1 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem$TemplateItemBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem$TemplateItemBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem.class
index 215563a..fb3a77e 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateItem.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo$TemplateSectionInfoBuilder.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo$TemplateSectionInfoBuilder.class
index 066b95a..02b5c99 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo$TemplateSectionInfoBuilder.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo$TemplateSectionInfoBuilder.class differ
diff --git a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo.class b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo.class
index 45a9669..f1d3113 100644
Binary files a/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo.class and b/meeting/bin/main/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse$TemplateSectionInfo.class differ
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/Meeting.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/Meeting.java
index f5c147e..b6b7f10 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/Meeting.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/Meeting.java
@@ -119,7 +119,20 @@ public class Meeting {
public void addParticipant(String participantEmail) {
if (this.participants == null) {
this.participants = new ArrayList<>();
+ } else if (!(this.participants instanceof ArrayList)) {
+ // 불변 리스트인 경우 새로운 ArrayList로 변환
+ this.participants = new ArrayList<>(this.participants);
}
- this.participants.add(participantEmail);
+
+ if (!this.participants.contains(participantEmail)) {
+ this.participants.add(participantEmail);
+ }
+ }
+
+ /**
+ * 템플릿 적용
+ */
+ public void applyTemplate(String templateId) {
+ this.templateId = templateId;
}
}
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/MeetingAnalysis.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/MeetingAnalysis.java
new file mode 100644
index 0000000..57ff112
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/domain/MeetingAnalysis.java
@@ -0,0 +1,122 @@
+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 MeetingAnalysis {
+
+ /**
+ * 분석 ID
+ */
+ private String analysisId;
+
+ /**
+ * 회의 ID
+ */
+ private String meetingId;
+
+ /**
+ * 회의록 ID
+ */
+ private String minutesId;
+
+ /**
+ * 주요 키워드
+ */
+ private List keywords;
+
+ /**
+ * 안건별 분석 결과
+ */
+ private List agendaAnalyses;
+
+ /**
+ * 전체 회의 분석 상태 (PENDING, IN_PROGRESS, COMPLETED, FAILED)
+ */
+ private String status;
+
+ /**
+ * 분석 완료 시간
+ */
+ private LocalDateTime completedAt;
+
+ /**
+ * 생성 시간
+ */
+ private LocalDateTime createdAt;
+
+ /**
+ * 분석 완료 처리
+ */
+ public void complete() {
+ this.status = "COMPLETED";
+ this.completedAt = LocalDateTime.now();
+ }
+
+ /**
+ * 분석 실패 처리
+ */
+ public void fail() {
+ this.status = "FAILED";
+ }
+
+ /**
+ * 분석 진행 중 처리
+ */
+ public void startAnalysis() {
+ this.status = "IN_PROGRESS";
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class AgendaAnalysis {
+ /**
+ * 안건 ID
+ */
+ private String agendaId;
+
+ /**
+ * 안건 제목
+ */
+ private String title;
+
+ /**
+ * AI 요약 (간략)
+ */
+ private String aiSummaryShort;
+
+ /**
+ * 논의 주제
+ */
+ private String discussion;
+
+ /**
+ * 결정 사항
+ */
+ private List decisions;
+
+ /**
+ * 보류 사항
+ */
+ private List pending;
+
+ /**
+ * 추출된 Todo 목록
+ */
+ private List extractedTodos;
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/dto/MeetingEndDTO.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/dto/MeetingEndDTO.java
new file mode 100644
index 0000000..7e38d6a
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/dto/MeetingEndDTO.java
@@ -0,0 +1,45 @@
+package com.unicorn.hgzero.meeting.biz.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * 회의 종료 비즈니스 DTO
+ */
+@Getter
+@Builder
+public class MeetingEndDTO {
+
+ private final String title;
+ private final int participantCount;
+ private final int durationMinutes;
+ private final int agendaCount;
+ private final int todoCount;
+ private final List keywords;
+ private final List agendaSummaries;
+
+ @Getter
+ @Builder
+ public static class AgendaSummaryDTO {
+ private final String title;
+ private final String aiSummaryShort;
+ private final AgendaDetailsDTO details;
+ private final List todos;
+ }
+
+ @Getter
+ @Builder
+ public static class AgendaDetailsDTO {
+ private final String discussion;
+ private final List decisions;
+ private final List pending;
+ }
+
+ @Getter
+ @Builder
+ public static class TodoSummaryDTO {
+ private final String title;
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/ApplyTemplateService.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/ApplyTemplateService.java
new file mode 100644
index 0000000..2058f39
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/ApplyTemplateService.java
@@ -0,0 +1,44 @@
+package com.unicorn.hgzero.meeting.biz.service;
+
+import com.unicorn.hgzero.meeting.biz.domain.Meeting;
+import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.ApplyTemplateUseCase;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 회의록 템플릿 적용 서비스
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@Transactional
+public class ApplyTemplateService implements ApplyTemplateUseCase {
+
+ private final MeetingReader meetingReader;
+ private final MeetingWriter meetingWriter;
+
+ @Override
+ public Meeting applyTemplate(ApplyTemplateCommand command) {
+ log.debug("템플릿 적용 시작 - meetingId: {}, templateId: {}",
+ command.meetingId(), command.templateId());
+
+ // 회의 조회
+ Meeting meeting = meetingReader.findById(command.meetingId())
+ .orElseThrow(() -> new IllegalArgumentException("회의를 찾을 수 없습니다: " + command.meetingId()));
+
+ // 템플릿 적용 (회의 도메인에서 처리)
+ meeting.applyTemplate(command.templateId());
+
+ // 회의 정보 업데이트
+ Meeting updatedMeeting = meetingWriter.save(meeting);
+
+ log.debug("템플릿 적용 완료 - meetingId: {}, templateId: {}",
+ command.meetingId(), command.templateId());
+
+ return updatedMeeting;
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/MeetingService.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/MeetingService.java
index 0513df2..9b17301 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/MeetingService.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/service/MeetingService.java
@@ -3,11 +3,16 @@ package com.unicorn.hgzero.meeting.biz.service;
import com.unicorn.hgzero.common.exception.BusinessException;
import com.unicorn.hgzero.common.exception.ErrorCode;
import com.unicorn.hgzero.meeting.biz.domain.Meeting;
+import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
import com.unicorn.hgzero.meeting.biz.domain.Minutes;
import com.unicorn.hgzero.meeting.biz.domain.Session;
+import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
import com.unicorn.hgzero.meeting.biz.usecase.in.meeting.*;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingWriter;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisReader;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisWriter;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.MinutesWriter;
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionReader;
import com.unicorn.hgzero.meeting.biz.usecase.out.SessionWriter;
@@ -42,7 +47,10 @@ public class MeetingService implements
private final MeetingWriter meetingWriter;
private final SessionReader sessionReader;
private final SessionWriter sessionWriter;
+ private final MinutesReader minutesReader;
private final MinutesWriter minutesWriter;
+ private final MeetingAnalysisReader meetingAnalysisReader;
+ private final MeetingAnalysisWriter meetingAnalysisWriter;
private final CacheService cacheService;
private final EventPublisher eventPublisher;
private final com.unicorn.hgzero.meeting.biz.usecase.out.ParticipantWriter participantWriter;
@@ -263,30 +271,165 @@ public class MeetingService implements
}
/**
- * 회의 종료
+ * 회의 종료 및 AI 분석
*/
@Override
@Transactional
- public Meeting endMeeting(String meetingId) {
+ public MeetingEndDTO endMeeting(String meetingId) {
log.info("Ending meeting: {}", meetingId);
- // 회의 조회
+ // 1. 회의 조회
+ log.debug("Searching for meeting with ID: {}", meetingId);
Meeting meeting = meetingReader.findById(meetingId)
- .orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
+ .orElseThrow(() -> {
+ log.error("Meeting not found: {}", meetingId);
+ return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의를 찾을 수 없습니다: " + meetingId);
+ });
+ log.debug("Found meeting: {}, status: {}", meeting.getTitle(), meeting.getStatus());
- // 회의 상태 검증
- if (!"IN_PROGRESS".equals(meeting.getStatus())) {
- throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE);
+ // 2. 회의 상태 검증 (SCHEDULED 또는 IN_PROGRESS만 종료 가능)
+ if (!"SCHEDULED".equals(meeting.getStatus()) && !"IN_PROGRESS".equals(meeting.getStatus())) {
+ log.warn("Invalid meeting status for ending: meetingId={}, status={}", meetingId, meeting.getStatus());
+ throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE,
+ "회의를 종료할 수 없는 상태입니다. 현재 상태: " + meeting.getStatus());
}
- // 회의 종료
+ // 3. 회의 종료
meeting.end();
-
- // 저장
Meeting updatedMeeting = meetingWriter.save(meeting);
+ // 4. 회의록 조회 (SCHEDULED 상태면 빈 회의록 생성, IN_PROGRESS면 기존 회의록 조회)
+ Minutes minutes;
+ if ("SCHEDULED".equals(meeting.getStatus())) {
+ // SCHEDULED 상태에서 종료하는 경우 빈 회의록 생성
+ String minutesId = UUID.randomUUID().toString();
+ minutes = Minutes.builder()
+ .minutesId(minutesId)
+ .meetingId(meetingId)
+ .title(meeting.getTitle() + " - 회의록")
+ .sections(List.of())
+ .status("DRAFT")
+ .version(1)
+ .createdBy(meeting.getOrganizerId())
+ .createdAt(LocalDateTime.now())
+ .build();
+ minutesWriter.save(minutes);
+ log.info("Empty minutes created for SCHEDULED meeting: meetingId={}, minutesId={}", meetingId, minutesId);
+ } else {
+ // IN_PROGRESS 상태면 기존 회의록 조회
+ log.debug("Searching for existing minutes for meeting: {}", meetingId);
+ minutes = minutesReader.findLatestByMeetingId(meetingId)
+ .orElseThrow(() -> {
+ log.error("Minutes not found for meeting: {}", meetingId);
+ return new BusinessException(ErrorCode.ENTITY_NOT_FOUND, "회의록을 찾을 수 없습니다: " + meetingId);
+ });
+ log.debug("Found minutes: {}", minutes.getTitle());
+ }
+
+ // 5. AI 분석 수행 (현재는 Mock 데이터로 구현)
+ MeetingAnalysis analysis = performAIAnalysis(meeting, minutes);
+ meetingAnalysisWriter.save(analysis);
+
+ // 6. 결과 DTO 구성
+ MeetingEndDTO result = buildMeetingEndDTO(meeting, analysis);
+
log.info("Meeting ended successfully: {}", meetingId);
- return updatedMeeting;
+ return result;
+ }
+
+ /**
+ * AI 분석 수행 (Mock 구현)
+ */
+ private MeetingAnalysis performAIAnalysis(Meeting meeting, Minutes minutes) {
+ log.info("Performing AI analysis for meeting: {}", meeting.getMeetingId());
+
+ // Mock 데이터로 구현 (실제로는 AI 서비스 호출)
+ List keywords = List.of(
+ "#신제품기획", "#예산편성", "#일정조율",
+ "#시장조사", "#UI/UX", "#개발스펙"
+ );
+
+ List agendaAnalyses = List.of(
+ MeetingAnalysis.AgendaAnalysis.builder()
+ .agendaId("agenda-1")
+ .title("1. 신제품 기획 방향성")
+ .aiSummaryShort("타겟 고객을 20-30대로 설정, UI/UX 개선 집중")
+ .discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고, 기존 제품 대비 UI/UX를 대폭 개선하기로 함")
+ .decisions(List.of("타겟 고객: 20-30대 직장인", "UI/UX 개선을 최우선 과제로 설정"))
+ .pending(List.of())
+ .extractedTodos(List.of("시장 조사 보고서 작성", "UI/UX 개선안 초안 작성"))
+ .build(),
+ MeetingAnalysis.AgendaAnalysis.builder()
+ .agendaId("agenda-2")
+ .title("2. 예산 편성 및 일정")
+ .aiSummaryShort("총 예산 5억, 개발 기간 6개월 확정")
+ .discussion("신제품 개발을 위한 총 예산을 5억원으로 책정하고, 개발 기간은 6개월로 확정함")
+ .decisions(List.of("총 예산: 5억원", "개발 기간: 6개월", "예산 배분: 개발 60%, 마케팅 40%"))
+ .pending(List.of("세부 일정 확정은 다음 회의에서 논의"))
+ .extractedTodos(List.of("세부 개발 일정 수립"))
+ .build(),
+ MeetingAnalysis.AgendaAnalysis.builder()
+ .agendaId("agenda-3")
+ .title("3. 기술 스택 및 개발 방향")
+ .aiSummaryShort("React 기반 프론트엔드, AI 챗봇 기능 추가")
+ .discussion("프론트엔드는 React 기반으로 개발하고, 고객 지원을 위한 AI 챗봇 기능을 추가하기로 함")
+ .decisions(List.of("프론트엔드: React 기반", "AI 챗봇 기능 추가", "Next.js 도입 검토"))
+ .pending(List.of("AI 챗봇 학습 데이터 확보 방안"))
+ .extractedTodos(List.of("AI 챗봇 프로토타입 개발", "Next.js 도입 검토 보고서"))
+ .build()
+ );
+
+ return MeetingAnalysis.builder()
+ .analysisId(UUID.randomUUID().toString())
+ .meetingId(meeting.getMeetingId())
+ .minutesId(minutes.getMinutesId())
+ .keywords(keywords)
+ .agendaAnalyses(agendaAnalyses)
+ .status("COMPLETED")
+ .completedAt(LocalDateTime.now())
+ .createdAt(LocalDateTime.now())
+ .build();
+ }
+
+ /**
+ * MeetingEndDTO 구성
+ */
+ private MeetingEndDTO buildMeetingEndDTO(Meeting meeting, MeetingAnalysis analysis) {
+ // 회의 시간 계산 (Mock)
+ int durationMinutes = 90;
+
+ // 전체 Todo 개수 계산
+ int totalTodos = analysis.getAgendaAnalyses().stream()
+ .mapToInt(agenda -> agenda.getExtractedTodos().size())
+ .sum();
+
+ // 안건별 요약 DTO 변환
+ List agendaSummaries = analysis.getAgendaAnalyses().stream()
+ .map(agenda -> MeetingEndDTO.AgendaSummaryDTO.builder()
+ .title(agenda.getTitle())
+ .aiSummaryShort(agenda.getAiSummaryShort())
+ .details(MeetingEndDTO.AgendaDetailsDTO.builder()
+ .discussion(agenda.getDiscussion())
+ .decisions(agenda.getDecisions())
+ .pending(agenda.getPending())
+ .build())
+ .todos(agenda.getExtractedTodos().stream()
+ .map(todo -> MeetingEndDTO.TodoSummaryDTO.builder()
+ .title(todo)
+ .build())
+ .toList())
+ .build())
+ .toList();
+
+ return MeetingEndDTO.builder()
+ .title(meeting.getTitle())
+ .participantCount(meeting.getParticipants() != null ? meeting.getParticipants().size() : 0)
+ .durationMinutes(durationMinutes)
+ .agendaCount(analysis.getAgendaAnalyses().size())
+ .todoCount(totalTodos)
+ .keywords(analysis.getKeywords())
+ .agendaSummaries(agendaSummaries)
+ .build();
}
/**
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/ApplyTemplateUseCase.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/ApplyTemplateUseCase.java
new file mode 100644
index 0000000..dd09e1c
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/ApplyTemplateUseCase.java
@@ -0,0 +1,25 @@
+package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
+
+import com.unicorn.hgzero.meeting.biz.domain.Meeting;
+
+/**
+ * 회의록 템플릿 적용 UseCase
+ */
+public interface ApplyTemplateUseCase {
+
+ /**
+ * 회의에 템플릿을 적용
+ *
+ * @param command 템플릿 적용 명령
+ * @return 업데이트된 회의 정보
+ */
+ Meeting applyTemplate(ApplyTemplateCommand command);
+
+ /**
+ * 템플릿 적용 명령
+ */
+ record ApplyTemplateCommand(
+ String meetingId,
+ String templateId
+ ) {}
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/EndMeetingUseCase.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/EndMeetingUseCase.java
index 900292a..04856c9 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/EndMeetingUseCase.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/in/meeting/EndMeetingUseCase.java
@@ -1,6 +1,6 @@
package com.unicorn.hgzero.meeting.biz.usecase.in.meeting;
-import com.unicorn.hgzero.meeting.biz.domain.Meeting;
+import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
/**
* 회의 종료 UseCase
@@ -8,7 +8,7 @@ import com.unicorn.hgzero.meeting.biz.domain.Meeting;
public interface EndMeetingUseCase {
/**
- * 회의 종료
+ * 회의 종료 및 AI 분석
*/
- Meeting endMeeting(String meetingId);
+ MeetingEndDTO endMeeting(String meetingId);
}
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisReader.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisReader.java
new file mode 100644
index 0000000..9f2eba3
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisReader.java
@@ -0,0 +1,26 @@
+package com.unicorn.hgzero.meeting.biz.usecase.out;
+
+import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
+
+import java.util.Optional;
+
+/**
+ * 회의 분석 Reader
+ */
+public interface MeetingAnalysisReader {
+
+ /**
+ * 회의 ID로 분석 결과 조회
+ */
+ Optional findByMeetingId(String meetingId);
+
+ /**
+ * 분석 ID로 분석 결과 조회
+ */
+ Optional findById(String analysisId);
+
+ /**
+ * 회의록 ID로 분석 결과 조회
+ */
+ Optional findByMinutesId(String minutesId);
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisWriter.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisWriter.java
new file mode 100644
index 0000000..568f25e
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/biz/usecase/out/MeetingAnalysisWriter.java
@@ -0,0 +1,19 @@
+package com.unicorn.hgzero.meeting.biz.usecase.out;
+
+import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
+
+/**
+ * 회의 분석 Writer
+ */
+public interface MeetingAnalysisWriter {
+
+ /**
+ * 분석 결과 저장
+ */
+ MeetingAnalysis save(MeetingAnalysis analysis);
+
+ /**
+ * 분석 결과 삭제
+ */
+ void delete(String analysisId);
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/DashboardController.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/DashboardController.java
index 01accf0..c223763 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/DashboardController.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/DashboardController.java
@@ -1,8 +1,6 @@
package com.unicorn.hgzero.meeting.infra.controller;
import com.unicorn.hgzero.common.dto.ApiResponse;
-import com.unicorn.hgzero.meeting.biz.dto.DashboardDTO;
-import com.unicorn.hgzero.meeting.biz.usecase.in.dashboard.GetDashboardUseCase;
import com.unicorn.hgzero.meeting.infra.dto.response.DashboardResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -14,6 +12,11 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
/**
* 대시보드 REST API Controller
* 사용자별 맞춤 대시보드 데이터 제공
@@ -25,17 +28,15 @@ import org.springframework.web.bind.annotation.*;
@Slf4j
public class DashboardController {
- private final GetDashboardUseCase getDashboardUseCase;
-
/**
- * 대시보드 데이터 조회
+ * 대시보드 데이터 조회 (목 데이터)
*
* @param userId 사용자 ID
* @return 대시보드 데이터
*/
@Operation(
summary = "대시보드 데이터 조회",
- description = "사용자별 맞춤 대시보드 정보를 조회합니다. 예정된 회의 목록, 진행 중 Todo 목록, 최근 회의록 목록, 통계 정보를 포함합니다.",
+ description = "사용자별 맞춤 대시보드 정보를 조회합니다. 예정된 회의 목록, 최근 회의록 목록, 통계 정보를 포함합니다.",
security = @SecurityRequirement(name = "bearerAuth")
)
@GetMapping
@@ -49,12 +50,80 @@ public class DashboardController {
log.info("대시보드 데이터 조회 요청 - userId: {}", userId);
- var dashboardData = getDashboardUseCase.getDashboard(userId);
- var dashboardDTO = DashboardDTO.from(dashboardData);
- var response = DashboardResponse.from(dashboardDTO);
+ // 목 데이터 생성
+ DashboardResponse mockResponse = createMockDashboardData();
log.info("대시보드 데이터 조회 완료 - userId: {}", userId);
- return ResponseEntity.ok(ApiResponse.success(response));
+ return ResponseEntity.ok(ApiResponse.success(mockResponse));
+ }
+
+ /**
+ * 목 데이터 생성
+ */
+ private DashboardResponse createMockDashboardData() {
+ // 예정된 회의 목 데이터
+ List upcomingMeetings = Arrays.asList(
+ DashboardResponse.UpcomingMeetingResponse.builder()
+ .meetingId("550e8400-e29b-41d4-a716-446655440001")
+ .title("Q1 전략 회의")
+ .startTime(LocalDateTime.now().plusDays(2).withHour(14).withMinute(0))
+ .endTime(LocalDateTime.now().plusDays(2).withHour(16).withMinute(0))
+ .location("회의실 A")
+ .participantCount(5)
+ .status("SCHEDULED")
+ .build(),
+ DashboardResponse.UpcomingMeetingResponse.builder()
+ .meetingId("550e8400-e29b-41d4-a716-446655440002")
+ .title("개발팀 스프린트 계획")
+ .startTime(LocalDateTime.now().plusDays(3).withHour(10).withMinute(0))
+ .endTime(LocalDateTime.now().plusDays(3).withHour(12).withMinute(0))
+ .location("회의실 B")
+ .participantCount(8)
+ .status("SCHEDULED")
+ .build()
+ );
+
+ // 최근 회의록 목 데이터
+ List recentMinutes = Arrays.asList(
+ DashboardResponse.RecentMinutesResponse.builder()
+ .minutesId("770e8400-e29b-41d4-a716-446655440001")
+ .title("아키텍처 설계 회의")
+ .meetingDate(LocalDateTime.now().minusDays(1).withHour(14).withMinute(0))
+ .status("FINALIZED")
+ .participantCount(6)
+ .lastModified(LocalDateTime.now().minusDays(1).withHour(16).withMinute(30))
+ .build(),
+ DashboardResponse.RecentMinutesResponse.builder()
+ .minutesId("770e8400-e29b-41d4-a716-446655440002")
+ .title("UI/UX 검토 회의")
+ .meetingDate(LocalDateTime.now().minusDays(3).withHour(11).withMinute(0))
+ .status("FINALIZED")
+ .participantCount(4)
+ .lastModified(LocalDateTime.now().minusDays(3).withHour(12).withMinute(45))
+ .build(),
+ DashboardResponse.RecentMinutesResponse.builder()
+ .minutesId("770e8400-e29b-41d4-a716-446655440003")
+ .title("API 설계 검토")
+ .meetingDate(LocalDateTime.now().minusDays(5).withHour(15).withMinute(0))
+ .status("DRAFT")
+ .participantCount(3)
+ .lastModified(LocalDateTime.now().minusDays(5).withHour(16).withMinute(15))
+ .build()
+ );
+
+ // 통계 정보 목 데이터
+ DashboardResponse.StatisticsResponse statistics = DashboardResponse.StatisticsResponse.builder()
+ .upcomingMeetingsCount(2)
+ .activeTodosCount(0) // activeTodos 제거로 0으로 설정
+ .todoCompletionRate(0.0) // activeTodos 제거로 0으로 설정
+ .build();
+
+ return DashboardResponse.builder()
+ .upcomingMeetings(upcomingMeetings)
+ .activeTodos(Collections.emptyList()) // activeTodos 빈 리스트로 설정
+ .myMinutes(recentMinutes)
+ .statistics(statistics)
+ .build();
}
}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/MeetingController.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/MeetingController.java
index 36eab4c..9976d17 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/MeetingController.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/MeetingController.java
@@ -8,6 +8,7 @@ import com.unicorn.hgzero.meeting.infra.dto.request.InviteParticipantRequest;
import com.unicorn.hgzero.meeting.infra.dto.request.SelectTemplateRequest;
import com.unicorn.hgzero.meeting.infra.dto.response.InviteParticipantResponse;
import com.unicorn.hgzero.meeting.infra.dto.response.MeetingResponse;
+import com.unicorn.hgzero.meeting.infra.dto.response.MeetingEndResponse;
import com.unicorn.hgzero.meeting.infra.dto.response.SessionResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -20,6 +21,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
+import java.util.List;
/**
* 회의 관리 REST API Controller
@@ -38,6 +40,7 @@ public class MeetingController {
private final GetMeetingUseCase getMeetingUseCase;
private final CancelMeetingUseCase cancelMeetingUseCase;
private final InviteParticipantUseCase inviteParticipantUseCase;
+ private final ApplyTemplateUseCase applyTemplateUseCase;
/**
* 회의 예약
@@ -113,11 +116,17 @@ public class MeetingController {
log.info("템플릿 적용 요청 - meetingId: {}, templateId: {}", meetingId, request.getTemplateId());
- var meetingData = getMeetingUseCase.getMeeting(meetingId);
+ var meetingData = applyTemplateUseCase.applyTemplate(
+ new ApplyTemplateUseCase.ApplyTemplateCommand(
+ meetingId,
+ request.getTemplateId()
+ )
+ );
+
var meetingDTO = MeetingDTO.from(meetingData);
var response = MeetingResponse.from(meetingDTO);
- log.info("템플릿 적용 완료 - meetingId: {}", meetingId);
+ log.info("템플릿 적용 완료 - meetingId: {}, templateId: {}", meetingId, request.getTemplateId());
return ResponseEntity.ok(ApiResponse.success(response));
}
@@ -162,15 +171,15 @@ public class MeetingController {
*
* @param meetingId 회의 ID
* @param userId 사용자 ID
- * @return 회의 정보
+ * @return 회의 종료 결과
*/
@Operation(
summary = "회의 종료",
- description = "진행 중인 회의를 종료하고 회의록 작성을 완료합니다. 자동 Todo 추출 및 알림 발송이 수행됩니다.",
+ description = "진행 중인 회의를 종료하고 AI 분석을 통해 회의록을 생성합니다. 주요 키워드 추출, 안건별 요약, Todo 자동 추출이 수행됩니다.",
security = @SecurityRequirement(name = "bearerAuth")
)
@PostMapping("/{meetingId}/end")
- public ResponseEntity> endMeeting(
+ public ResponseEntity> endMeeting(
@Parameter(description = "회의 ID", required = true)
@PathVariable String meetingId,
@Parameter(description = "사용자 ID", required = true)
@@ -182,15 +191,102 @@ public class MeetingController {
log.info("회의 종료 요청 - meetingId: {}, userId: {}", meetingId, userId);
- var meetingData = endMeetingUseCase.endMeeting(meetingId);
- var meetingDTO = MeetingDTO.from(meetingData);
- var response = MeetingResponse.from(meetingDTO);
+ // Mock 데이터로 응답 (개발용)
+ var response = createMockMeetingEndResponse(meetingId);
- log.info("회의 종료 완료 - meetingId: {}", meetingId);
+ log.info("회의 종료 완료 (Mock) - meetingId: {}", meetingId);
return ResponseEntity.ok(ApiResponse.success(response));
}
+ /**
+ * 회의 종료 응답 Mock 데이터 생성
+ *
+ * @param meetingId 회의 ID
+ * @return Mock 회의 종료 응답
+ */
+ private MeetingEndResponse createMockMeetingEndResponse(String meetingId) {
+ return MeetingEndResponse.builder()
+ .title("Q1 전략 기획 회의")
+ .participantCount(4)
+ .durationMinutes(90)
+ .agendaCount(3)
+ .todoCount(5)
+ .keywords(List.of("신제품 기획", "마케팅 전략", "예산 계획", "UI/UX 개선", "고객 분석"))
+ .agendaSummaries(List.of(
+ MeetingEndResponse.AgendaSummary.builder()
+ .title("1. 신제품 기획 방향성")
+ .aiSummaryShort("타겟 고객을 20-30대로 설정하고 UI/UX 개선에 집중하기로 결정")
+ .details(MeetingEndResponse.AgendaDetails.builder()
+ .discussion("신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고 모바일 중심의 사용자 경험을 강화하는 방향으로 논의됨")
+ .decisions(List.of(
+ "타겟 고객: 20-30대 직장인",
+ "플랫폼: 모바일 우선",
+ "핵심 기능: 간편 결제, 개인화 추천"
+ ))
+ .pending(List.of(
+ "경쟁사 분석 보완 필요",
+ "기술 스택 최종 검토"
+ ))
+ .build())
+ .todos(List.of(
+ MeetingEndResponse.TodoSummary.builder()
+ .title("시장 조사 보고서 작성")
+ .build(),
+ MeetingEndResponse.TodoSummary.builder()
+ .title("와이어프레임 초안 제작")
+ .build()
+ ))
+ .build(),
+ MeetingEndResponse.AgendaSummary.builder()
+ .title("2. 마케팅 전략 수립")
+ .aiSummaryShort("SNS 마케팅과 인플루언서 협업을 통한 브랜드 인지도 향상 계획")
+ .details(MeetingEndResponse.AgendaDetails.builder()
+ .discussion("초기 론칭 시 SNS 중심의 마케팅 전략과 마이크로 인플루언서 협업을 통한 브랜드 인지도 향상 방안 논의")
+ .decisions(List.of(
+ "마케팅 채널: 인스타그램, 틱톡 우선",
+ "예산 배분: 인플루언서 50%, 광고 30%, 이벤트 20%",
+ "론칭 시기: 2024년 2분기"
+ ))
+ .pending(List.of(
+ "인플루언서 리스트 검토",
+ "마케팅 예산 최종 승인"
+ ))
+ .build())
+ .todos(List.of(
+ MeetingEndResponse.TodoSummary.builder()
+ .title("인플루언서 후보 리스트 작성")
+ .build(),
+ MeetingEndResponse.TodoSummary.builder()
+ .title("마케팅 예산안 상세 작성")
+ .build()
+ ))
+ .build(),
+ MeetingEndResponse.AgendaSummary.builder()
+ .title("3. 프로젝트 일정 및 리소스")
+ .aiSummaryShort("개발 6개월, 테스트 2개월로 총 8개월 일정 확정")
+ .details(MeetingEndResponse.AgendaDetails.builder()
+ .discussion("전체 프로젝트 일정을 8개월로 설정하고 개발팀 6명, 디자인팀 2명으로 팀 구성 확정")
+ .decisions(List.of(
+ "전체 일정: 8개월 (개발 6개월, 테스트 2개월)",
+ "팀 구성: 개발 6명, 디자인 2명, PM 1명",
+ "주요 마일스톤: MVP 3개월, 베타 6개월, 정식 출시 8개월"
+ ))
+ .pending(List.of(
+ "개발자 추가 채용 검토",
+ "외부 업체 협업 범위 논의"
+ ))
+ .build())
+ .todos(List.of(
+ MeetingEndResponse.TodoSummary.builder()
+ .title("개발자 채용 공고 작성")
+ .build()
+ ))
+ .build()
+ ))
+ .build();
+ }
+
/**
* 회의 정보 조회
*
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/TemplateController.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/TemplateController.java
index 01e7f1c..a232e82 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/TemplateController.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/controller/TemplateController.java
@@ -1,13 +1,8 @@
package com.unicorn.hgzero.meeting.infra.controller;
import com.unicorn.hgzero.common.dto.ApiResponse;
-import com.unicorn.hgzero.meeting.biz.dto.TemplateDTO;
-import com.unicorn.hgzero.meeting.biz.service.TemplateService;
import com.unicorn.hgzero.meeting.infra.dto.response.TemplateListResponse;
-import com.unicorn.hgzero.meeting.infra.dto.response.TemplateDetailResponse;
-import com.unicorn.hgzero.meeting.infra.cache.CacheService;
import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@@ -15,12 +10,14 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
/**
- * 템플릿 관리 API Controller
- * 템플릿 목록 조회, 상세 조회 기능
+ * 템플릿 관리API Controller
+ * 고정된 템플릿 목록을 제공합니다
*/
@RestController
@RequestMapping("/api/templates")
@@ -29,11 +26,8 @@ import java.util.stream.Collectors;
@Tag(name = "Template", description = "템플릿 관리 API")
public class TemplateController {
- private final TemplateService templateService;
- private final CacheService cacheService;
-
/**
- * 템플릿 목록 조회
+ * 템플릿 목록 조회 (고정 데이터 반환)
* GET /api/templates
*/
@GetMapping
@@ -45,40 +39,19 @@ public class TemplateController {
})
public ResponseEntity> getTemplateList(
@RequestHeader("X-User-Id") String userId,
- @RequestHeader("X-User-Name") String userName,
- @Parameter(description = "템플릿 카테고리") @RequestParam(required = false) String category,
- @Parameter(description = "활성 상태 (true: 활성, false: 비활성)") @RequestParam(required = false) Boolean isActive) {
+ @RequestHeader("X-User-Name") String userName) {
- log.info("템플릿 목록 조회 요청 - userId: {}, category: {}, isActive: {}",
- userId, category, isActive);
+ log.info("템플릿 목록 조회 요청 - userId: {}", userId);
try {
- // 캐시 확인
- String cacheKey = String.format("templates:list:%s:%s",
- (category != null ? category : "all"),
- (isActive != null ? isActive.toString() : "all"));
- TemplateListResponse cachedResponse = cacheService.getCachedTemplateList(cacheKey);
- if (cachedResponse != null) {
- log.debug("캐시된 템플릿 목록 반환");
- return ResponseEntity.ok(ApiResponse.success(cachedResponse));
- }
-
- // 템플릿 목록 조회
- List templates = templateService.getTemplateList(category, isActive);
-
- // 응답 DTO 생성
- List templateItems = templates.stream()
- .map(this::convertToTemplateItem)
- .collect(Collectors.toList());
+ // 고정된 템플릿 데이터 생성
+ List templateItems = createFixedTemplates();
TemplateListResponse response = TemplateListResponse.builder()
.templateList(templateItems)
.totalCount(templateItems.size())
.build();
- // 캐시 저장
- cacheService.cacheTemplateList(cacheKey, response);
-
log.info("템플릿 목록 조회 성공 - count: {}", templateItems.size());
return ResponseEntity.ok(ApiResponse.success(response));
@@ -90,99 +63,103 @@ public class TemplateController {
}
/**
- * 템플릿 상세 조회
- * GET /api/templates/{templateId}
+ * 고정된 템플릿 데이터 생성
*/
- @GetMapping("/{templateId}")
- @Operation(summary = "템플릿 상세 조회", description = "템플릿 상세 정보를 조회합니다")
- public ResponseEntity> getTemplateDetail(
- @RequestHeader("X-User-Id") String userId,
- @RequestHeader("X-User-Name") String userName,
- @Parameter(description = "템플릿 ID") @PathVariable String templateId) {
-
- log.info("템플릿 상세 조회 요청 - userId: {}, templateId: {}", userId, templateId);
-
- try {
- // 캐시 확인
- TemplateDetailResponse cachedResponse = cacheService.getCachedTemplateDetail(templateId);
- if (cachedResponse != null) {
- log.debug("캐시된 템플릿 상세 반환 - templateId: {}", templateId);
- return ResponseEntity.ok(ApiResponse.success(cachedResponse));
- }
-
- // 템플릿 조회
- TemplateDTO templateDTO = templateService.getTemplateById(templateId);
-
- // 응답 DTO 생성
- TemplateDetailResponse response = convertToTemplateDetailResponse(templateDTO);
-
- // 캐시 저장
- cacheService.cacheTemplateDetail(templateId, response);
-
- log.info("템플릿 상세 조회 성공 - templateId: {}", templateId);
- return ResponseEntity.ok(ApiResponse.success(response));
-
- } catch (Exception e) {
- log.error("템플릿 상세 조회 실패 - templateId: {}", templateId, e);
- return ResponseEntity.badRequest()
- .body(ApiResponse.errorWithType("템플릿 상세 조회에 실패했습니다"));
- }
+ private List createFixedTemplates() {
+ List templates = new ArrayList<>();
+
+ // 일반 회의 템플릿
+ templates.add(TemplateListResponse.TemplateItem.builder()
+ .templateId("general")
+ .name("일반 회의")
+ .description("기본 회의록 형식")
+ .category("meeting")
+ .icon("📋")
+ .isActive(true)
+ .usageCount(0)
+ .createdAt(LocalDateTime.now())
+ .lastUsedAt(null)
+ .createdBy("system")
+ .sections(Arrays.asList(
+ createSectionInfo("회의 개요", "회의 기본 정보", 1, true),
+ createSectionInfo("논의 사항", "주요 논의 내용", 2, true),
+ createSectionInfo("결정 사항", "회의에서 결정된 사항", 3, true),
+ createSectionInfo("액션 아이템", "향후 진행할 작업", 4, true)
+ ))
+ .build());
+
+ // 스크럼 회의 템플릿
+ templates.add(TemplateListResponse.TemplateItem.builder()
+ .templateId("scrum")
+ .name("스크럼 회의")
+ .description("데일리 스탠드업 형식")
+ .category("agile")
+ .icon("🏃")
+ .isActive(true)
+ .usageCount(0)
+ .createdAt(LocalDateTime.now())
+ .lastUsedAt(null)
+ .createdBy("system")
+ .sections(Arrays.asList(
+ createSectionInfo("어제 한 일", "지난 작업일에 완료한 작업", 1, true),
+ createSectionInfo("오늘 할 일", "오늘 진행할 예정 작업", 2, true),
+ createSectionInfo("블로커/이슈", "진행을 방해하는 요소", 3, false)
+ ))
+ .build());
+
+ // 킥오프 회의 템플릿
+ templates.add(TemplateListResponse.TemplateItem.builder()
+ .templateId("kickoff")
+ .name("킥오프 회의")
+ .description("프로젝트 시작 회의")
+ .category("project")
+ .icon("🚀")
+ .isActive(true)
+ .usageCount(0)
+ .createdAt(LocalDateTime.now())
+ .lastUsedAt(null)
+ .createdBy("system")
+ .sections(Arrays.asList(
+ createSectionInfo("프로젝트 개요", "프로젝트 기본 정보", 1, true),
+ createSectionInfo("목표 및 범위", "프로젝트 목표와 범위", 2, true),
+ createSectionInfo("역할 및 책임", "팀원별 역할과 책임", 3, true),
+ createSectionInfo("일정 및 마일스톤", "프로젝트 일정", 4, true)
+ ))
+ .build());
+
+ // 주간 회의 템플릿
+ templates.add(TemplateListResponse.TemplateItem.builder()
+ .templateId("weekly")
+ .name("주간 회의")
+ .description("주간 리뷰 및 계획")
+ .category("review")
+ .icon("📅")
+ .isActive(true)
+ .usageCount(0)
+ .createdAt(LocalDateTime.now())
+ .lastUsedAt(null)
+ .createdBy("system")
+ .sections(Arrays.asList(
+ createSectionInfo("지난주 성과", "지난주 달성한 성과", 1, true),
+ createSectionInfo("이번주 계획", "이번주 진행할 계획", 2, true),
+ createSectionInfo("주요 이슈", "해결이 필요한 이슈", 3, false),
+ createSectionInfo("다음 액션", "다음 주 액션 아이템", 4, true)
+ ))
+ .build());
+
+ return templates;
}
- // Helper methods
- private TemplateListResponse.TemplateItem convertToTemplateItem(TemplateDTO templateDTO) {
- // 섹션 정보 변환
- List sections = templateDTO.getSections().stream()
- .map(section -> TemplateListResponse.TemplateSectionInfo.builder()
- .title(section.getTitle())
- .description(section.getDescription())
- .orderIndex(section.getOrderIndex())
- .isRequired(section.isRequired())
- .build())
- .collect(Collectors.toList());
-
- return TemplateListResponse.TemplateItem.builder()
- .templateId(templateDTO.getTemplateId())
- .name(templateDTO.getName())
- .description(templateDTO.getDescription())
- .category(templateDTO.getCategory())
- .isActive(templateDTO.isActive())
- .usageCount(templateDTO.getUsageCount())
- .createdAt(templateDTO.getCreatedAt())
- .lastUsedAt(templateDTO.getLastUsedAt())
- .createdBy(templateDTO.getCreatedBy())
- .sections(sections)
- .build();
- }
-
- private TemplateDetailResponse convertToTemplateDetailResponse(TemplateDTO templateDTO) {
- // 섹션 상세 정보 변환
- List sections = templateDTO.getSections().stream()
- .map(section -> TemplateDetailResponse.SectionDetail.builder()
- .sectionId(section.getSectionId())
- .title(section.getTitle())
- .description(section.getDescription())
- .content(section.getContent())
- .orderIndex(section.getOrderIndex())
- .isRequired(section.isRequired())
- .inputType(section.getInputType())
- .placeholder(section.getPlaceholder())
- .maxLength(section.getMaxLength())
- .isEditable(section.isEditable())
- .build())
- .collect(Collectors.toList());
-
- return TemplateDetailResponse.builder()
- .templateId(templateDTO.getTemplateId())
- .name(templateDTO.getName())
- .description(templateDTO.getDescription())
- .category(templateDTO.getCategory())
- .isActive(templateDTO.isActive())
- .usageCount(templateDTO.getUsageCount())
- .createdAt(templateDTO.getCreatedAt())
- .lastUsedAt(templateDTO.getLastUsedAt())
- .createdBy(templateDTO.getCreatedBy())
- .sections(sections)
+ /**
+ * 템플릿 섹션 정보 생성 헬퍼 메서드
+ */
+ private TemplateListResponse.TemplateSectionInfo createSectionInfo(
+ String title, String description, int orderIndex, boolean isRequired) {
+ return TemplateListResponse.TemplateSectionInfo.builder()
+ .title(title)
+ .description(description)
+ .orderIndex(orderIndex)
+ .isRequired(isRequired)
.build();
}
}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/SelectTemplateRequest.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/SelectTemplateRequest.java
index 8fe3b0b..51b858b 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/SelectTemplateRequest.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/request/SelectTemplateRequest.java
@@ -17,9 +17,7 @@ import jakarta.validation.constraints.NotBlank;
public class SelectTemplateRequest {
@NotBlank(message = "템플릿 ID는 필수입니다")
- @Schema(description = "템플릿 ID", example = "template-001", required = true)
+ @Schema(description = "템플릿 ID", example = "general", required = true,
+ allowableValues = {"general", "scrum", "kickoff", "weekly"})
private String templateId;
-
- @Schema(description = "커스터마이징 옵션", example = "섹션 순서 변경 또는 추가 섹션 포함")
- private String customization;
}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/MeetingEndResponse.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/MeetingEndResponse.java
new file mode 100644
index 0000000..1bfe8b1
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/MeetingEndResponse.java
@@ -0,0 +1,120 @@
+package com.unicorn.hgzero.meeting.infra.dto.response;
+
+import com.unicorn.hgzero.meeting.biz.dto.MeetingEndDTO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 회의 종료 응답 DTO
+ */
+@Getter
+@Builder
+@Schema(description = "회의 종료 응답")
+public class MeetingEndResponse {
+
+ @Schema(description = "회의 제목", example = "Q1 전략 회의")
+ private final String title;
+
+ @Schema(description = "참석자 수", example = "4")
+ private final int participantCount;
+
+ @Schema(description = "회의 시간 (분)", example = "90")
+ private final int durationMinutes;
+
+ @Schema(description = "주요 안건 수", example = "3")
+ private final int agendaCount;
+
+ @Schema(description = "Todo 생성 수", example = "5")
+ private final int todoCount;
+
+ @Schema(description = "주요 키워드")
+ private final List keywords;
+
+ @Schema(description = "안건별 AI 요약")
+ private final List agendaSummaries;
+
+ /**
+ * MeetingEndDTO로부터 MeetingEndResponse 생성
+ */
+ public static MeetingEndResponse from(MeetingEndDTO dto) {
+ return MeetingEndResponse.builder()
+ .title(dto.getTitle())
+ .participantCount(dto.getParticipantCount())
+ .durationMinutes(dto.getDurationMinutes())
+ .agendaCount(dto.getAgendaCount())
+ .todoCount(dto.getTodoCount())
+ .keywords(dto.getKeywords())
+ .agendaSummaries(dto.getAgendaSummaries().stream()
+ .map(AgendaSummary::from)
+ .toList())
+ .build();
+ }
+
+ @Getter
+ @Builder
+ @Schema(description = "안건 요약 정보")
+ public static class AgendaSummary {
+ @Schema(description = "안건 제목", example = "1. 신제품 기획 방향성")
+ private final String title;
+
+ @Schema(description = "AI 요약 (간략)", example = "타겟 고객을 20-30대로 설정, UI/UX 개선 집중")
+ private final String aiSummaryShort;
+
+ @Schema(description = "상세 내용")
+ private final AgendaDetails details;
+
+ @Schema(description = "Todo 목록")
+ private final List todos;
+
+ public static AgendaSummary from(MeetingEndDTO.AgendaSummaryDTO dto) {
+ return AgendaSummary.builder()
+ .title(dto.getTitle())
+ .aiSummaryShort(dto.getAiSummaryShort())
+ .details(AgendaDetails.from(dto.getDetails()))
+ .todos(dto.getTodos().stream()
+ .map(TodoSummary::from)
+ .toList())
+ .build();
+ }
+ }
+
+ @Getter
+ @Builder
+ @Schema(description = "안건 상세 내용")
+ public static class AgendaDetails {
+ @Schema(description = "논의 주제", example = "신제품의 주요 타겟 고객층을 20-30대 직장인으로 설정하고...")
+ private final String discussion;
+
+ @Schema(description = "결정 사항")
+ private final List decisions;
+
+ @Schema(description = "보류 사항")
+ private final List pending;
+
+ public static AgendaDetails from(MeetingEndDTO.AgendaDetailsDTO dto) {
+ return AgendaDetails.builder()
+ .discussion(dto.getDiscussion())
+ .decisions(dto.getDecisions())
+ .pending(dto.getPending())
+ .build();
+ }
+ }
+
+ @Getter
+ @Builder
+ @Schema(description = "Todo 요약 정보")
+ public static class TodoSummary {
+ @Schema(description = "Todo 제목", example = "시장 조사 보고서 작성")
+ private final String title;
+
+ public static TodoSummary from(MeetingEndDTO.TodoSummaryDTO dto) {
+ return TodoSummary.builder()
+ .title(dto.getTitle())
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse.java
index 3d600b8..80edb9f 100644
--- a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse.java
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/dto/response/TemplateListResponse.java
@@ -29,6 +29,7 @@ public class TemplateListResponse {
private String name;
private String description;
private String category;
+ private String icon; // 아이콘 추가
private boolean isActive;
private int usageCount;
private LocalDateTime createdAt;
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/MeetingAnalysisGateway.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/MeetingAnalysisGateway.java
new file mode 100644
index 0000000..4ac0aa0
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/MeetingAnalysisGateway.java
@@ -0,0 +1,58 @@
+package com.unicorn.hgzero.meeting.infra.gateway;
+
+import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisReader;
+import com.unicorn.hgzero.meeting.biz.usecase.out.MeetingAnalysisWriter;
+import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
+import com.unicorn.hgzero.meeting.infra.gateway.repository.MeetingAnalysisJpaRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+
+/**
+ * 회의 분석 Gateway
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MeetingAnalysisGateway implements MeetingAnalysisReader, MeetingAnalysisWriter {
+
+ private final MeetingAnalysisJpaRepository repository;
+
+ @Override
+ public Optional findByMeetingId(String meetingId) {
+ log.debug("Finding meeting analysis by meetingId: {}", meetingId);
+ return repository.findByMeetingId(meetingId)
+ .map(MeetingAnalysisEntity::toDomain);
+ }
+
+ @Override
+ public Optional findById(String analysisId) {
+ log.debug("Finding meeting analysis by analysisId: {}", analysisId);
+ return repository.findById(analysisId)
+ .map(MeetingAnalysisEntity::toDomain);
+ }
+
+ @Override
+ public Optional findByMinutesId(String minutesId) {
+ log.debug("Finding meeting analysis by minutesId: {}", minutesId);
+ return repository.findByMinutesId(minutesId)
+ .map(MeetingAnalysisEntity::toDomain);
+ }
+
+ @Override
+ public MeetingAnalysis save(MeetingAnalysis analysis) {
+ log.debug("Saving meeting analysis: {}", analysis.getAnalysisId());
+ MeetingAnalysisEntity entity = MeetingAnalysisEntity.fromDomain(analysis);
+ MeetingAnalysisEntity savedEntity = repository.save(entity);
+ return savedEntity.toDomain();
+ }
+
+ @Override
+ public void delete(String analysisId) {
+ log.debug("Deleting meeting analysis: {}", analysisId);
+ repository.deleteById(analysisId);
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MeetingAnalysisEntity.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MeetingAnalysisEntity.java
new file mode 100644
index 0000000..1caca97
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/entity/MeetingAnalysisEntity.java
@@ -0,0 +1,84 @@
+package com.unicorn.hgzero.meeting.infra.gateway.entity;
+
+import com.unicorn.hgzero.meeting.biz.domain.MeetingAnalysis;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 회의 분석 결과 Entity
+ */
+@Entity
+@Table(name = "meeting_analysis")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MeetingAnalysisEntity {
+
+ @Id
+ @Column(name = "analysis_id")
+ private String analysisId;
+
+ @Column(name = "meeting_id", nullable = false)
+ private String meetingId;
+
+ @Column(name = "minutes_id", nullable = false)
+ private String minutesId;
+
+ @ElementCollection
+ @CollectionTable(name = "meeting_keywords", joinColumns = @JoinColumn(name = "analysis_id"))
+ @Column(name = "keyword")
+ private List keywords;
+
+ @Column(name = "agenda_analyses", columnDefinition = "TEXT")
+ private String agendaAnalysesJson; // JSON 문자열로 저장
+
+ @Column(name = "status")
+ private String status;
+
+ @Column(name = "completed_at")
+ private LocalDateTime completedAt;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ /**
+ * Entity를 도메인으로 변환
+ */
+ public MeetingAnalysis toDomain() {
+ // JSON 파싱은 실제 구현에서는 ObjectMapper 사용
+ // 현재는 Mock으로 처리
+ return MeetingAnalysis.builder()
+ .analysisId(this.analysisId)
+ .meetingId(this.meetingId)
+ .minutesId(this.minutesId)
+ .keywords(this.keywords)
+ .agendaAnalyses(List.of()) // Mock
+ .status(this.status)
+ .completedAt(this.completedAt)
+ .createdAt(this.createdAt)
+ .build();
+ }
+
+ /**
+ * 도메인에서 Entity로 변환
+ */
+ public static MeetingAnalysisEntity fromDomain(MeetingAnalysis domain) {
+ return MeetingAnalysisEntity.builder()
+ .analysisId(domain.getAnalysisId())
+ .meetingId(domain.getMeetingId())
+ .minutesId(domain.getMinutesId())
+ .keywords(domain.getKeywords())
+ .agendaAnalysesJson("{}") // Mock JSON
+ .status(domain.getStatus())
+ .completedAt(domain.getCompletedAt())
+ .createdAt(domain.getCreatedAt())
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/repository/MeetingAnalysisJpaRepository.java b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/repository/MeetingAnalysisJpaRepository.java
new file mode 100644
index 0000000..70b9c5c
--- /dev/null
+++ b/meeting/src/main/java/com/unicorn/hgzero/meeting/infra/gateway/repository/MeetingAnalysisJpaRepository.java
@@ -0,0 +1,24 @@
+package com.unicorn.hgzero.meeting.infra.gateway.repository;
+
+import com.unicorn.hgzero.meeting.infra.gateway.entity.MeetingAnalysisEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+/**
+ * 회의 분석 JPA Repository
+ */
+@Repository
+public interface MeetingAnalysisJpaRepository extends JpaRepository {
+
+ /**
+ * 회의 ID로 분석 결과 조회
+ */
+ Optional findByMeetingId(String meetingId);
+
+ /**
+ * 회의록 ID로 분석 결과 조회
+ */
+ Optional findByMinutesId(String minutesId);
+}
\ No newline at end of file