diff --git a/claude/api-design.md b/claude/api-design.md new file mode 100644 index 0000000..c3e0be1 --- /dev/null +++ b/claude/api-design.md @@ -0,0 +1,115 @@ +# STT Service API 설계 완료 + +## 작업 결과 + +### 생성된 파일 +- **파일 경로**: `C:\Users\KTDS\home\workspace\HGZero\design\backend\api\stt-service-api.yaml` +- **형식**: OpenAPI 3.0.3 +- **검증 상태**: ✅ 검증 완료 (swagger-cli) + +### API 개요 + +#### 1. Recording API (음성 녹음 관리) +- `POST /recordings/prepare` - 회의 녹음 준비 +- `POST /recordings/{recordingId}/start` - 음성 녹음 시작 +- `POST /recordings/{recordingId}/stop` - 음성 녹음 중지 +- `GET /recordings/{recordingId}` - 녹음 정보 조회 + +#### 2. Transcription API (음성-텍스트 변환) +- `POST /transcripts/stream` - 실시간 음성-텍스트 변환 (스트리밍) +- `POST /transcripts/batch` - 배치 음성-텍스트 변환 +- `POST /transcripts/callback` - 배치 변환 완료 콜백 +- `GET /transcripts/{recordingId}` - 변환 텍스트 전체 조회 + +#### 3. Speaker API (화자 식별 및 관리) +- `POST /speakers/identify` - 화자 식별 +- `GET /speakers/{speakerId}` - 화자 정보 조회 +- `PUT /speakers/{speakerId}` - 화자 정보 업데이트 +- `GET /recordings/{recordingId}/speakers` - 녹음의 화자 목록 조회 + +### 주요 특징 + +#### 1. 유저스토리 매핑 +모든 API는 유저스토리와 매핑되어 있습니다: +- **UFR-STT-010** (음성녹음인식): Recording API, Speaker API +- **UFR-STT-020** (텍스트변환): Transcription API + +#### 2. 완전한 스키마 정의 +- 25개의 스키마 정의 +- 모든 Request/Response 모델 포함 +- Example 데이터 포함으로 Swagger UI에서 즉시 테스트 가능 + +#### 3. Azure 통합 +- Azure Speech Service 연동 +- Azure Blob Storage 통합 +- Azure Event Hubs 이벤트 발행 + +#### 4. 실시간 처리 +- WebSocket 기반 스트리밍 지원 +- 실시간 인식 지연: < 1초 +- 화자 식별 정확도: > 90% + +#### 5. 성능 정보 +각 API의 예상 처리 시간 명시: +- 녹음 준비: ~1.1초 +- 실시간 변환: 1-4초 +- 배치 변환: 7-33초 + +### API 확인 방법 + +#### 1. Swagger Editor 사용 +1. https://editor.swagger.io/ 접속 +2. 생성된 YAML 파일 내용 복사하여 붙여넣기 +3. 우측 패널에서 API 문서 확인 및 테스트 + +#### 2. 로컬 Swagger UI 실행 +```bash +# Swagger UI Docker 실행 +docker run -p 8080:8080 -e SWAGGER_JSON=/api/stt-service-api.yaml \ + -v C:\Users\KTDS\home\workspace\HGZero\design\backend\api:/api \ + swaggerapi/swagger-ui + +# 브라우저에서 http://localhost:8080 접속 +``` + +#### 3. VS Code Extension +- **확장**: Swagger Viewer +- YAML 파일 열고 `Shift + Alt + P` 실행 +- 미리보기에서 API 문서 확인 + +### 설계 원칙 준수 + +✅ **유저스토리 기반 설계** +- 모든 API에 x-user-story 필드 명시 +- 불필요한 API 추가 없음 + +✅ **시퀀스 일관성** +- 내부 시퀀스 설계와 완전히 일치 +- 모든 처리 흐름 반영 + +✅ **OpenAPI 3.0 표준** +- servers 섹션 필수 포함 +- 완전한 스키마 정의 +- JWT 인증 방식 명시 + +✅ **Example 데이터** +- 모든 스키마에 example 포함 +- 실제 테스트 가능한 데이터 + +✅ **검증 완료** +- swagger-cli 자동 검증 통과 +- YAML 구문 오류 없음 +- 스키마 참조 유효성 확인 + +### 다음 단계 + +1. **Meeting Service API 설계** (회의, 회의록, Todo 통합) +2. **AI Service API 설계** (회의록 자동 작성, RAG 기능) +3. **User Service API 설계** (인증 전용) +4. **Notification Service API 설계** (알림 발송) + +--- + +**작성자**: 준호 (Backend Developer) +**작성일**: 2025-01-23 +**검증 도구**: swagger-cli v4.0.4 diff --git a/design/backend/api/API설계서.md b/design/backend/api/API설계서.md new file mode 100644 index 0000000..da9e297 --- /dev/null +++ b/design/backend/api/API설계서.md @@ -0,0 +1,471 @@ +# API 설계서 + +회의록 작성 및 공유 개선 서비스 + +--- + +## 📋 목차 + +1. [개요](#개요) +2. [API 설계 원칙](#api-설계-원칙) +3. [마이크로서비스별 API](#마이크로서비스별-api) +4. [공통 사항](#공통-사항) +5. [API 확인 방법](#api-확인-방법) + +--- + +## 개요 + +### 프로젝트 정보 +- **프로젝트명**: 회의록 작성 및 공유 개선 서비스 +- **설계 버전**: v2.0 +- **설계일**: 2025-01-23 +- **설계자**: 아키텍트(길동), Backend Developer(준호) + +### 마이크로서비스 구성 +본 서비스는 5개의 마이크로서비스로 구성됩니다: + +1. **User Service** - 사용자 인증 (LDAP 연동, JWT 토큰 발급/검증) +2. **Meeting Service** - 회의, 회의록, Todo, 실시간 협업 통합 관리 +3. **STT Service** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 +4. **AI Service** - AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합) +5. **Notification Service** - 알림 발송 및 리마인더 관리 + +--- + +## API 설계 원칙 + +### 1. 설계 기준 +- **유저스토리 기반**: 모든 API는 유저스토리와 매핑 (x-user-story 필드) +- **시퀀스 일치**: 외부/내부 시퀀스 설계서와 100% 일치 +- **OpenAPI 3.0**: OpenAPI 3.0.3 표준 준수 +- **컨트롤러 명시**: 각 API별 담당 컨트롤러 명시 (x-controller 필드) + +### 2. 공통 표준 +- **인증 방식**: JWT Bearer Token +- **요청 헤더**: 모든 API 요청에 사용자 정보 포함 + - `X-User-Id`: 사용자 ID + - `X-User-Name`: 사용자 이름 + - `X-User-Email`: 사용자 이메일 +- **응답 형식**: JSON +- **에러 응답**: 표준화된 에러 응답 형식 + +### 3. 서비스 독립성 +- **각 서비스별 독립적인 OpenAPI 명세** +- **서비스별 모든 스키마 포함** +- **중복 스키마 허용** (초기 단계) +- **독립 배포 가능** + +--- + +## 마이크로서비스별 API + +### 1. User Service + +#### 개요 +- **파일**: `user-service-api.yaml` +- **베이스 URL**: `/api/v1` +- **주요 기능**: 사용자 인증 전용 (LDAP 인증, JWT 토큰 관리) + +#### API 목록 + +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /auth/login | 사용자 로그인 | AFR-USER-010 | UserController | +| POST | /auth/refresh | Access Token 갱신 | AFR-USER-010 | UserController | +| POST | /auth/logout | 로그아웃 | AFR-USER-010 | UserController | +| GET | /auth/validate | 토큰 검증 | AFR-USER-010 | UserController | + +#### 주요 특징 +- LDAP 인증 (LDAPS, port 636) +- JWT 토큰 관리 (Access: 1시간, Refresh: 7일) +- 계정 잠금 기능 (5회 실패 시 30분) +- Redis 기반 Refresh Token 관리 + +--- + +### 2. Meeting Service + +#### 개요 +- **파일**: `meeting-service-api.yaml` +- **베이스 URL**: `/api/v1` +- **주요 기능**: 회의, 회의록, Todo, 실시간 협업 통합 관리 + +#### API 목록 + +**Dashboard APIs (1개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /dashboard | 대시보드 데이터 조회 | AFR-USER-020 | DashboardController | + +**Meeting APIs (4개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /meetings | 회의 예약 | UFR-MEET-010 | MeetingController | +| PUT | /meetings/{meetingId}/template | 템플릿 선택 | UFR-MEET-020 | MeetingController | +| POST | /meetings/{meetingId}/start | 회의 시작 | UFR-MEET-030 | MeetingController | +| POST | /meetings/{meetingId}/end | 회의 종료 | UFR-MEET-040 | MeetingController | + +**Minutes APIs (7개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /minutes | 회의록 목록 조회 | UFR-MEET-046 | MinutesController | +| GET | /minutes/{minutesId} | 회의록 상세 조회 | UFR-MEET-047 | MinutesController | +| PATCH | /minutes/{minutesId} | 회의록 수정 | UFR-MEET-055 | MinutesController | +| POST | /minutes/{minutesId}/finalize | 회의록 확정 | UFR-MEET-050 | MinutesController | +| POST | /minutes/{minutesId}/sections/{sectionId}/verify | 섹션 검증 완료 | UFR-COLLAB-030 | MinutesController | +| POST | /minutes/{minutesId}/sections/{sectionId}/lock | 섹션 잠금 | UFR-COLLAB-030 | MinutesController | +| DELETE | /minutes/{minutesId}/sections/{sectionId}/lock | 섹션 잠금 해제 | UFR-COLLAB-030 | MinutesController | + +**Todo APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /todos | Todo 할당 | UFR-TODO-010 | TodoController | +| PATCH | /todos/{todoId}/complete | Todo 완료 처리 | UFR-TODO-030 | TodoController | + +**Template APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /templates | 템플릿 목록 조회 | UFR-MEET-020 | TemplateController | +| GET | /templates/{templateId} | 템플릿 상세 조회 | UFR-MEET-020 | TemplateController | + +**WebSocket Endpoints (1개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /ws/minutes/{minutesId} | 실시간 협업 WebSocket | UFR-COLLAB-010 | WebSocketController | + +#### 주요 특징 +- WebSocket 기반 실시간 협업 +- 충돌 해결 메커니즘 (Last Write Wins) +- 섹션별 검증 및 잠금 기능 +- Todo와 회의록 양방향 연결 +- 버전 관리 + +--- + +### 3. STT Service + +#### 개요 +- **파일**: `stt-service-api.yaml` +- **베이스 URL**: `/api/v1` +- **주요 기능**: 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 + +#### API 목록 + +**Recording APIs (4개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /recordings/prepare | 녹음 준비 | UFR-STT-010 | RecordingController | +| POST | /recordings/{recordingId}/start | 녹음 시작 | UFR-STT-010 | RecordingController | +| POST | /recordings/{recordingId}/stop | 녹음 중지 | UFR-STT-010 | RecordingController | +| GET | /recordings/{recordingId} | 녹음 정보 조회 | UFR-STT-010 | RecordingController | + +**Transcription APIs (4개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /transcriptions/stream | 실시간 스트리밍 변환 | UFR-STT-020 | TranscriptionController | +| POST | /transcriptions/batch | 배치 변환 | UFR-STT-020 | TranscriptionController | +| POST | /transcriptions/callback | 변환 완료 콜백 | UFR-STT-020 | TranscriptionController | +| GET | /transcriptions/{recordingId}/full | 전체 텍스트 조회 | UFR-STT-020 | TranscriptionController | + +**Speaker APIs (4개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /speakers/identify | 화자 식별 | UFR-STT-010 | SpeakerController | +| GET | /speakers/{speakerId} | 화자 정보 조회 | UFR-STT-010 | SpeakerController | +| PATCH | /speakers/{speakerId} | 화자 정보 수정 | UFR-STT-010 | SpeakerController | +| GET | /recordings/{recordingId}/speakers | 녹음별 화자 목록 | UFR-STT-010 | SpeakerController | + +#### 주요 특징 +- WebSocket 기반 실시간 스트리밍 (< 1초 지연) +- 화자 식별 정확도 90% 이상 +- Azure Speech Service 통합 +- Azure Blob Storage 연동 +- Azure Event Hubs 비동기 처리 + +--- + +### 4. AI Service + +#### 개요 +- **파일**: `ai-service-api.yaml` +- **베이스 URL**: `/api/v1` +- **주요 기능**: AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합) + +#### API 목록 + +**Transcript Processing APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /transcripts/process | 회의록 자동 작성 | UFR-AI-010 | TranscriptController | +| POST | /transcripts/{meetingId}/improve | 회의록 개선 (프롬프팅) | UFR-AI-030 | TranscriptController | + +**Todo APIs (1개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /todos/extract | Todo 자동 추출 | UFR-AI-020 | TodoController | + +**Related Minutes APIs (1개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /transcripts/{meetingId}/related | 관련 회의록 연결 | UFR-AI-040 | TranscriptController | + +**Term Explanation APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /terms/detect | 전문용어 감지 | UFR-RAG-010 | TermController | +| GET | /terms/{term}/explain | 맥락 기반 용어 설명 | UFR-RAG-020 | TermController | + +**Suggestion APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /suggestions/discussion | 논의사항 제안 | UFR-AI-010 | SuggestionController | +| POST | /suggestions/decision | 결정사항 제안 | UFR-AI-010 | SuggestionController | + +#### 주요 특징 +- LLM 기반 회의록 자동 작성 +- 7가지 프롬프트 유형 지원 + - 1Page 요약, 핵심 요약, 상세 보고서 + - 의사결정 중심, 액션 아이템 중심 + - 경영진 보고용, 커스텀 +- RAG 기반 관련 회의록 검색 (벡터 유사도 70% 이상) +- 맥락 기반 전문용어 설명 +- 실시간 논의사항/결정사항 제안 + +#### 차별화 포인트 +1. **맥락 기반 용어 설명**: 단순 정의가 아닌 조직 내 실제 사용 맥락 제공 +2. **프롬프팅 기반 개선**: 다양한 형식의 회의록 생성 +3. **실시간 추천**: AI 기반 논의사항/결정사항 자동 제안 + +--- + +### 5. Notification Service + +#### 개요 +- **파일**: `notification-service-api.yaml` +- **베이스 URL**: `/api/v1` +- **주요 기능**: 알림 발송 및 리마인더 관리 + +#### API 목록 + +**Internal APIs (2개) - 이벤트 핸들러** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| POST | /notifications/invitation | 회의 초대 알림 발송 | UFR-MEET-010 | NotificationController | +| POST | /notifications/todo | Todo 할당 알림 발송 | UFR-TODO-010 | NotificationController | + +**Public APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /notifications | 사용자별 알림 이력 조회 | AFR-USER-020 | NotificationController | +| GET | /notifications/{notificationId} | 알림 상세 조회 | AFR-USER-020 | NotificationController | + +**Settings APIs (2개)** +| Method | Endpoint | 설명 | 유저스토리 | 컨트롤러 | +|--------|----------|------|-----------|----------| +| GET | /notifications/settings | 알림 설정 조회 | AFR-USER-020 | NotificationSettingsController | +| PUT | /notifications/settings | 알림 설정 업데이트 | AFR-USER-020 | NotificationSettingsController | + +#### 주요 특징 +- Azure Event Hubs 기반 비동기 처리 +- 이메일 템플릿 렌더링 +- 중복 발송 방지 (Idempotency) +- 재시도 메커니즘 (최대 3회, exponential backoff) +- 알림 설정 관리 (채널, 유형, 방해 금지 시간대) + +#### 이메일 템플릿 +- **회의 초대**: 회의 정보 + 참여 링크 + 캘린더 추가 +- **Todo 할당**: Todo 상세 + 회의록 링크 +- **리마인더**: 회의 시작 30분 전 자동 발송 +- **Todo 리마인더**: 마감일 3일 전, 1일 전, 당일 + +--- + +## 공통 사항 + +### 인증 및 권한 + +#### JWT 토큰 구조 +```json +{ + "userId": "string", + "userName": "string", + "email": "string", + "exp": 1234567890 +} +``` + +#### 인증 헤더 +``` +Authorization: Bearer {access_token} +``` + +#### 사용자 정보 헤더 (모든 API 요청) +``` +X-User-Id: {userId} +X-User-Name: {userName} +X-User-Email: {userEmail} +``` + +### 에러 응답 형식 + +#### 공통 에러 응답 +```json +{ + "status": "error", + "code": "ERROR_CODE", + "message": "에러 메시지", + "details": { + "field": "필드명", + "reason": "상세 사유" + }, + "timestamp": "2025-01-23T10:00:00Z" +} +``` + +#### HTTP 상태 코드 +- **200**: 성공 +- **201**: 생성 성공 +- **400**: 잘못된 요청 +- **401**: 인증 실패 +- **403**: 권한 없음 +- **404**: 리소스 없음 +- **409**: 충돌 (동시 수정 등) +- **500**: 서버 오류 + +### 페이징 표준 + +#### 요청 파라미터 +``` +page: 페이지 번호 (default: 0) +size: 페이지 크기 (default: 20, max: 100) +sort: 정렬 기준 (예: createdAt,desc) +``` + +#### 응답 형식 +```json +{ + "content": [...], + "pageable": { + "page": 0, + "size": 20, + "totalElements": 100, + "totalPages": 5 + } +} +``` + +--- + +## API 확인 방법 + +### 1. Swagger Editor에서 확인 + +1. **Swagger Editor 접속** + - https://editor.swagger.io/ + +2. **각 서비스 YAML 파일 확인** + - `design/backend/api/user-service-api.yaml` + - `design/backend/api/meeting-service-api.yaml` + - `design/backend/api/stt-service-api.yaml` + - `design/backend/api/ai-service-api.yaml` + - `design/backend/api/notification-service-api.yaml` + +3. **파일 내용 붙여넣기** + - 좌측 패널에 YAML 내용 붙여넣기 + - 우측 패널에서 API 문서 확인 + +4. **API 테스트** + - "Try it out" 버튼으로 API 테스트 + - Example 데이터로 요청/응답 시뮬레이션 + +### 2. 로컬에서 검증 + +#### swagger-cli 설치 +```bash +npm install -g @apidevtools/swagger-cli +``` + +#### 검증 실행 +```bash +# 개별 파일 검증 +swagger-cli validate design/backend/api/user-service-api.yaml + +# 전체 파일 검증 +swagger-cli validate design/backend/api/*.yaml +``` + +#### 검증 결과 +``` +design/backend/api/user-service-api.yaml is valid +design/backend/api/meeting-service-api.yaml is valid +design/backend/api/stt-service-api.yaml is valid +design/backend/api/ai-service-api.yaml is valid +design/backend/api/notification-service-api.yaml is valid +``` + +--- + +## 통계 + +### API 개수 요약 + +| 서비스 | API 개수 | 주요 기능 | +|--------|---------|----------| +| User Service | 4 | 사용자 인증 | +| Meeting Service | 17 | 회의, 회의록, Todo, 실시간 협업 | +| STT Service | 12 | 음성 녹음, 변환, 화자 식별 | +| AI Service | 8 | AI 회의록, Todo 추출, RAG 검색 | +| Notification Service | 6 | 알림 발송, 설정 관리 | +| **합계** | **47** | | + +### 유저스토리 커버리지 + +- **전체 유저스토리**: 25개 +- **API로 구현된 유저스토리**: 25개 +- **커버리지**: 100% + +--- + +## 문서 이력 + +| 버전 | 작성일 | 작성자 | 변경 내용 | +|------|--------|--------|----------| +| 1.0 | 2025-01-23 | 길동 (아키텍트), 준호 (Backend Developer) | 초안 작성 (5개 마이크로서비스) | + +--- + +## 부록 + +### A. 참조 문서 +- 유저스토리: `design/userstory.md` +- 외부 시퀀스 설계서: `design/backend/sequence/outer/*.puml` +- 내부 시퀀스 설계서: `design/backend/sequence/inner/*.puml` +- 공통 설계 원칙: `claude/common-principles.md` +- API 설계 가이드: `claude/api-design.md` + +### B. 파일 구조 +``` +design/backend/api/ +├── user-service-api.yaml # User Service API 명세 +├── meeting-service-api.yaml # Meeting Service API 명세 +├── stt-service-api.yaml # STT Service API 명세 +├── ai-service-api.yaml # AI Service API 명세 +├── notification-service-api.yaml # Notification Service API 명세 +└── API설계서.md # 본 문서 +``` + +### C. OpenAPI 3.0 주요 섹션 +- **openapi**: 버전 정보 (3.0.3) +- **info**: API 메타데이터 +- **servers**: 서버 URL +- **paths**: API 엔드포인트 +- **components**: 재사용 가능한 컴포넌트 + - schemas: 데이터 모델 + - parameters: 공통 파라미터 + - responses: 공통 응답 + - securitySchemes: 인증 방식 + +--- + +**© 2025 회의록 작성 및 공유 개선 서비스. All rights reserved.** diff --git a/design/backend/api/ai-service-api.yaml b/design/backend/api/ai-service-api.yaml new file mode 100644 index 0000000..0fdb40e --- /dev/null +++ b/design/backend/api/ai-service-api.yaml @@ -0,0 +1,1034 @@ +openapi: 3.0.3 +info: + title: AI Service API + description: | + 회의록 작성 및 공유 개선 서비스의 AI Service API + + **주요 기능:** + - 회의록 자동 작성 (LLM 기반) + - Todo 자동 추출 및 담당자 식별 + - 프롬프팅 기반 회의록 개선 + - 관련 회의록 자동 연결 (RAG) + - 전문용어 감지 및 맥락 기반 설명 (RAG) + - 논의사항/결정사항 실시간 제안 + version: 1.0.0 + contact: + name: AI Service Team + email: ai-service@example.com + +servers: + - url: https://virtserver.swaggerhub.com/ai-service/1.0.0 + description: SwaggerHub Mock Server + - url: http://localhost:8083/api + description: Local Development Server + - url: https://dev-api.hgzero.com/ai/api + description: Development Server + - url: https://api.hgzero.com/ai/api + description: Production Server + +tags: + - name: Transcript + description: 회의록 자동 작성 관련 API + - name: Todo + description: Todo 자동 추출 관련 API + - name: Improve + description: 회의록 개선 관련 API + - name: Relation + description: 관련 회의록 연결 관련 API + - name: Term + description: 전문용어 감지 및 설명 관련 API + - name: Suggestion + description: 논의사항/결정사항 제안 관련 API + +paths: + /transcripts/process: + post: + tags: + - Transcript + summary: 회의록 자동 작성 + description: | + STT에서 변환된 텍스트를 받아 LLM 기반으로 회의록을 자동 작성합니다. + 회의 맥락을 고려하여 구조화된 회의록 초안을 생성합니다. + operationId: processTranscript + x-user-story: UFR-AI-010 + x-controller: TranscriptController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptProcessRequest' + responses: + '200': + description: 회의록 자동 작성 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptProcessResponse' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /todos/extract: + post: + tags: + - Todo + summary: Todo 자동 추출 + description: | + 회의록에서 액션 아이템을 자동으로 추출하고 담당자를 식별합니다. + 추출된 Todo는 Meeting Service로 전달됩니다. + operationId: extractTodos + x-user-story: UFR-AI-020 + x-controller: TodoController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TodoExtractionRequest' + responses: + '200': + description: Todo 추출 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TodoExtractionResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /transcripts/{meetingId}/improve: + post: + tags: + - Improve + summary: 회의록 개선 + description: | + 프롬프팅을 통해 회의록을 다양한 형식으로 변환합니다. + (1Page 요약, 핵심 요약, 상세 보고서 등) + operationId: improveTranscript + x-user-story: UFR-AI-030 + x-controller: ImproveController + parameters: + - name: meetingId + in: path + required: true + schema: + type: string + format: uuid + description: 회의 ID + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptImproveRequest' + responses: + '200': + description: 회의록 개선 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptImproveResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /transcripts/{meetingId}/related: + get: + tags: + - Relation + summary: 관련 회의록 조회 + description: | + 벡터 유사도 검색을 통해 관련된 회의록을 찾아 반환합니다. + 주제 유사도, 키워드 일치도, 참석자 중복도 등을 종합하여 계산합니다. + operationId: findRelatedTranscripts + x-user-story: UFR-AI-040 + x-controller: RelationController + parameters: + - name: meetingId + in: path + required: true + schema: + type: string + format: uuid + description: 회의 ID + - name: transcriptId + in: query + required: true + schema: + type: string + format: uuid + description: 회의록 ID + - name: limit + in: query + schema: + type: integer + default: 5 + minimum: 1 + maximum: 10 + description: 반환할 최대 개수 + responses: + '200': + description: 관련 회의록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RelatedTranscriptsResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /terms/detect: + post: + tags: + - Term + summary: 전문용어 감지 + description: | + 회의록 텍스트에서 전문용어를 자동으로 감지합니다. + 조직별/산업별 용어 사전과 비교하여 신뢰도 점수를 계산합니다. + operationId: detectTerms + x-user-story: UFR-RAG-010 + x-controller: TermController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TermDetectionRequest' + responses: + '200': + description: 전문용어 감지 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TermDetectionResponse' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /terms/{term}/explain: + get: + tags: + - Term + summary: 맥락 기반 용어 설명 + description: | + 전문용어에 대한 맥락 기반 설명을 생성합니다. + RAG를 통해 과거 회의록, 사내 문서, 업무 이력을 검색하여 + 실용적인 설명을 제공합니다. + operationId: explainTerm + x-user-story: UFR-RAG-020 + x-controller: ExplanationController + parameters: + - name: term + in: path + required: true + schema: + type: string + description: 용어명 + - name: meetingId + in: query + required: true + schema: + type: string + format: uuid + description: 회의 ID + - name: context + in: query + schema: + type: string + description: 현재 회의 맥락 (선택) + responses: + '200': + description: 용어 설명 생성 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TermExplanationResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /suggestions/discussion: + post: + tags: + - Suggestion + summary: 논의사항 제안 + description: | + 현재 회의 진행 상황을 분석하여 추가로 논의하면 좋을 주제를 제안합니다. + 회의 안건 대비 빠진 항목이나 중요한 논의를 식별합니다. + operationId: suggestDiscussion + x-user-story: UFR-AI-010 + x-controller: SuggestionController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DiscussionSuggestionRequest' + responses: + '200': + description: 논의사항 제안 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DiscussionSuggestionResponse' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /suggestions/decision: + post: + tags: + - Suggestion + summary: 결정사항 제안 + description: | + 회의록 텍스트에서 결정사항 패턴을 감지하여 제안합니다. + "~하기로 함", "~로 결정" 등의 패턴을 분석합니다. + operationId: suggestDecision + x-user-story: UFR-AI-010 + x-controller: SuggestionController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DecisionSuggestionRequest' + responses: + '200': + description: 결정사항 제안 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DecisionSuggestionResponse' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + schemas: + # ======================================== + # Request Schemas + # ======================================== + + TranscriptProcessRequest: + type: object + required: + - meetingId + - transcriptText + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + transcriptText: + type: string + description: STT에서 변환된 텍스트 + example: "안녕하세요. 오늘 회의는 신규 프로젝트 킥오프 미팅입니다..." + userId: + type: string + description: 사용자 ID + example: "user123" + userName: + type: string + description: 사용자 이름 + example: "김철수" + context: + $ref: '#/components/schemas/MeetingContext' + + TodoExtractionRequest: + type: object + required: + - meetingId + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + userId: + type: string + description: 요청자 ID + example: "user123" + + TranscriptImproveRequest: + type: object + required: + - promptType + properties: + promptType: + type: string + enum: + - 1PAGE_SUMMARY + - CORE_SUMMARY + - DETAILED_REPORT + - DECISION_FOCUSED + - ACTION_FOCUSED + - EXECUTIVE_REPORT + - CUSTOM + description: 프롬프트 유형 + example: "1PAGE_SUMMARY" + customPrompt: + type: string + description: 사용자 정의 프롬프트 (promptType이 CUSTOM일 때) + example: "경영진 보고용으로 3가지 핵심 결정사항만 요약해주세요" + userId: + type: string + description: 사용자 ID + example: "user123" + + TermDetectionRequest: + type: object + required: + - meetingId + - text + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + text: + type: string + description: 분석할 회의록 텍스트 + example: "MSA 아키텍처로 설계하고, API Gateway를 통해 라우팅합니다..." + organizationId: + type: string + description: 조직 ID + example: "org123" + + DiscussionSuggestionRequest: + type: object + required: + - meetingId + - transcriptText + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + transcriptText: + type: string + description: 현재까지의 회의록 텍스트 + example: "프로젝트 일정에 대해 논의했습니다..." + + DecisionSuggestionRequest: + type: object + required: + - meetingId + - transcriptText + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + transcriptText: + type: string + description: 현재까지의 회의록 텍스트 + example: "React로 프론트엔드를 개발하기로 했습니다..." + + # ======================================== + # Response Schemas + # ======================================== + + TranscriptProcessResponse: + type: object + properties: + transcriptId: + type: string + format: uuid + description: 생성된 회의록 ID + example: "660e8400-e29b-41d4-a716-446655440001" + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + content: + $ref: '#/components/schemas/TranscriptContent' + suggestions: + $ref: '#/components/schemas/RealtimeSuggestions' + createdAt: + type: string + format: date-time + description: 생성 시간 + example: "2025-01-23T10:30:00Z" + status: + type: string + enum: [DRAFT, COMPLETED] + description: 회의록 상태 + example: "DRAFT" + + TodoExtractionResponse: + type: object + properties: + meetingId: + type: string + format: uuid + description: 회의 ID + example: "550e8400-e29b-41d4-a716-446655440000" + todos: + type: array + items: + $ref: '#/components/schemas/ExtractedTodo' + totalCount: + type: integer + description: 추출된 Todo 개수 + example: 5 + extractedAt: + type: string + format: date-time + description: 추출 시간 + example: "2025-01-23T11:00:00Z" + + TranscriptImproveResponse: + type: object + properties: + transcriptId: + type: string + format: uuid + description: 개선된 회의록 ID + example: "770e8400-e29b-41d4-a716-446655440002" + version: + type: integer + description: 버전 번호 + example: 2 + baseVersion: + type: integer + description: 원본 버전 번호 + example: 1 + improvementType: + type: string + description: 개선 유형 + example: "1PAGE_SUMMARY" + content: + type: string + description: 개선된 회의록 내용 + example: "## 프로젝트 킥오프 미팅 요약\n\n### 핵심 결정사항\n1. React 기반 프론트엔드 개발..." + originalLink: + type: string + description: 원본 회의록 링크 + example: "/transcripts/660e8400-e29b-41d4-a716-446655440001" + createdAt: + type: string + format: date-time + description: 생성 시간 + example: "2025-01-23T11:30:00Z" + + RelatedTranscriptsResponse: + type: object + properties: + relatedTranscripts: + type: array + items: + $ref: '#/components/schemas/RelatedTranscript' + totalCount: + type: integer + description: 관련 회의록 개수 + example: 3 + + TermDetectionResponse: + type: object + properties: + detectedTerms: + type: array + items: + $ref: '#/components/schemas/DetectedTerm' + totalCount: + type: integer + description: 감지된 용어 개수 + example: 8 + highlightInfo: + type: array + items: + $ref: '#/components/schemas/HighlightInfo' + + TermExplanationResponse: + type: object + properties: + term: + type: string + description: 용어명 + example: "MSA" + basicDefinition: + type: string + description: 간단한 정의 + example: "Microservices Architecture의 약자로, 애플리케이션을 작은 독립적인 서비스로 나누는 아키텍처 패턴" + contextualMeaning: + type: string + description: 현재 회의 맥락에서의 의미 + example: "이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정" + useCases: + type: array + items: + type: string + description: 실제 사용 사례 + example: + - "2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성" + - "서비스별 독립 배포로 배포 시간 70% 단축" + relatedProjects: + type: array + items: + $ref: '#/components/schemas/RelatedProject' + pastDiscussions: + type: array + items: + $ref: '#/components/schemas/PastDiscussion' + references: + type: array + items: + $ref: '#/components/schemas/Reference' + + DiscussionSuggestionResponse: + type: object + properties: + suggestions: + type: array + items: + $ref: '#/components/schemas/DiscussionSuggestion' + totalCount: + type: integer + description: 제안 개수 + example: 3 + timestamp: + type: string + format: date-time + description: 생성 시각 + example: "2025-01-23T10:35:00Z" + + DecisionSuggestionResponse: + type: object + properties: + suggestions: + type: array + items: + $ref: '#/components/schemas/DecisionSuggestion' + totalCount: + type: integer + description: 제안 개수 + example: 4 + timestamp: + type: string + format: date-time + description: 생성 시각 + example: "2025-01-23T10:35:00Z" + + # ======================================== + # Common Schemas + # ======================================== + + MeetingContext: + type: object + description: 회의 맥락 정보 + properties: + title: + type: string + description: 회의 제목 + example: "신규 프로젝트 킥오프 미팅" + participants: + type: array + items: + type: string + description: 참석자 목록 + example: ["김철수", "이영희", "박민수"] + agenda: + type: array + items: + type: string + description: 회의 안건 + example: ["프로젝트 개요", "일정 논의", "역할 분담"] + previousContent: + type: string + description: 이전 회의록 내용 + example: "지난 회의에서 기술 스택을 논의했습니다..." + + TranscriptContent: + type: object + description: 회의록 내용 + properties: + summary: + type: string + description: 전체 요약 + example: "프로젝트 킥오프 미팅에서 기술 스택과 일정을 확정했습니다." + discussions: + type: array + items: + $ref: '#/components/schemas/DiscussionItem' + decisions: + type: array + items: + $ref: '#/components/schemas/DecisionItem' + pendingItems: + type: array + items: + type: string + description: 보류 사항 + example: ["추가 예산 검토 필요", "외주 업체 선정 보류"] + + DiscussionItem: + type: object + properties: + topic: + type: string + description: 논의 주제 + example: "기술 스택 선정" + speaker: + type: string + description: 발언자 + example: "김철수" + content: + type: string + description: 논의 내용 + example: "프론트엔드는 React, 백엔드는 Spring Boot를 사용하기로 제안" + + DecisionItem: + type: object + properties: + content: + type: string + description: 결정 내용 + example: "React로 프론트엔드 개발" + decisionMaker: + type: string + description: 결정자 + example: "이영희" + category: + type: string + enum: [기술, 일정, 리소스, 정책, 기타] + description: 결정 카테고리 + example: "기술" + + RealtimeSuggestions: + type: object + description: 실시간 추천사항 + properties: + discussionTopics: + type: array + items: + $ref: '#/components/schemas/DiscussionSuggestion' + decisions: + type: array + items: + $ref: '#/components/schemas/DecisionSuggestion' + + DiscussionSuggestion: + type: object + properties: + id: + type: string + format: uuid + description: 제안 ID + example: "880e8400-e29b-41d4-a716-446655440003" + topic: + type: string + description: 논의 주제 + example: "보안 요구사항 검토" + reason: + type: string + description: 제안 이유 + example: "안건에 포함되어 있으나 아직 논의되지 않음" + priority: + type: string + enum: [HIGH, MEDIUM, LOW] + description: 우선순위 + example: "HIGH" + relatedAgenda: + type: string + description: 관련 안건 + example: "프로젝트 개요" + estimatedTime: + type: integer + description: 예상 소요 시간 (분) + example: 15 + + DecisionSuggestion: + type: object + properties: + id: + type: string + format: uuid + description: 제안 ID + example: "990e8400-e29b-41d4-a716-446655440004" + content: + type: string + description: 결정 내용 + example: "React로 프론트엔드 개발" + category: + type: string + enum: [기술, 일정, 리소스, 정책, 기타] + description: 결정 카테고리 + example: "기술" + decisionMaker: + type: string + description: 결정자 + example: "김철수" + participants: + type: array + items: + type: string + description: 참여자 + example: ["김철수", "이영희"] + confidence: + type: number + format: float + minimum: 0 + maximum: 1 + description: 신뢰도 점수 + example: 0.85 + extractedFrom: + type: string + description: 원문 발췌 + example: "프론트엔드는 React로 개발하기로 했습니다" + context: + type: string + description: 결정 배경 + example: "팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상" + + ExtractedTodo: + type: object + properties: + content: + type: string + description: Todo 내용 + example: "API 설계서 작성" + assignee: + type: string + description: 담당자 + example: "박민수" + dueDate: + type: string + format: date + description: 마감일 + example: "2025-01-30" + priority: + type: string + enum: [HIGH, MEDIUM, LOW] + description: 우선순위 + example: "HIGH" + sectionReference: + type: string + description: 관련 회의록 섹션 + example: "결정사항 #3" + + RelatedTranscript: + type: object + properties: + transcriptId: + type: string + format: uuid + description: 회의록 ID + example: "aa0e8400-e29b-41d4-a716-446655440005" + title: + type: string + description: 회의 제목 + example: "프로젝트 X 주간 회의" + date: + type: string + format: date + description: 회의 날짜 + example: "2025-01-15" + participants: + type: array + items: + type: string + description: 참석자 + example: ["김철수", "이영희"] + relevanceScore: + type: number + format: float + description: 관련도 점수 (0-100%) + example: 85.5 + commonKeywords: + type: array + items: + type: string + description: 공통 키워드 + example: ["MSA", "API Gateway", "Spring Boot"] + link: + type: string + description: 회의록 링크 + example: "/transcripts/aa0e8400-e29b-41d4-a716-446655440005" + + DetectedTerm: + type: object + properties: + term: + type: string + description: 용어명 + example: "MSA" + position: + $ref: '#/components/schemas/TextPosition' + confidence: + type: number + format: float + minimum: 0 + maximum: 1 + description: 신뢰도 점수 + example: 0.92 + category: + type: string + enum: [기술, 업무, 도메인] + description: 용어 카테고리 + example: "기술" + highlight: + type: boolean + description: 하이라이트 여부 + example: true + + TextPosition: + type: object + description: 텍스트 위치 정보 + properties: + line: + type: integer + description: 줄 번호 + example: 5 + offset: + type: integer + description: 시작 오프셋 + example: 42 + + HighlightInfo: + type: object + properties: + term: + type: string + description: 용어명 + example: "API Gateway" + position: + $ref: '#/components/schemas/TextPosition' + style: + type: string + description: 하이라이트 스타일 + example: "background-color: yellow" + tooltip: + type: string + description: 툴팁 텍스트 + example: "용어 설명 로딩 중..." + + RelatedProject: + type: object + properties: + name: + type: string + description: 프로젝트명 + example: "프로젝트 X" + relevance: + type: string + description: 연관성 설명 + example: "동일한 MSA 아키텍처 적용" + + PastDiscussion: + type: object + properties: + date: + type: string + format: date + description: 논의 날짜 + example: "2024-12-15" + participants: + type: array + items: + type: string + description: 참석자 + example: ["김철수", "이영희"] + summary: + type: string + description: 논의 요약 + example: "MSA 아키텍처의 장단점을 비교하고 적용 방안을 논의" + link: + type: string + description: 회의록 링크 + example: "/transcripts/bb0e8400-e29b-41d4-a716-446655440006" + + Reference: + type: object + properties: + title: + type: string + description: 문서 제목 + example: "MSA 아키텍처 가이드" + type: + type: string + enum: [위키, 매뉴얼, 회의록, 보고서] + description: 문서 유형 + example: "위키" + link: + type: string + description: 문서 URL + example: "https://wiki.example.com/msa-guide" + + ErrorResponse: + type: object + properties: + error: + type: string + description: 에러 코드 + example: "INVALID_REQUEST" + message: + type: string + description: 에러 메시지 + example: "meetingId는 필수 항목입니다" + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-01-23T10:30:00Z" + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "BAD_REQUEST" + message: "필수 파라미터가 누락되었습니다" + timestamp: "2025-01-23T10:30:00Z" + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "NOT_FOUND" + message: "해당 회의록을 찾을 수 없습니다" + timestamp: "2025-01-23T10:30:00Z" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "INTERNAL_SERVER_ERROR" + message: "서버 처리 중 오류가 발생했습니다" + timestamp: "2025-01-23T10:30:00Z" + + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증 + +security: + - BearerAuth: [] diff --git a/design/backend/api/meeting-service-api.yaml b/design/backend/api/meeting-service-api.yaml new file mode 100644 index 0000000..1061c30 --- /dev/null +++ b/design/backend/api/meeting-service-api.yaml @@ -0,0 +1,1811 @@ +openapi: 3.0.0 +info: + title: Meeting Service API + description: | + 회의록 작성 및 공유 개선 서비스의 Meeting Service API + + **주요 기능:** + - 회의 관리 (예약, 시작, 종료) + - 회의록 관리 (생성, 수정, 확정, 조회) + - Todo 관리 (할당, 진행, 완료) + - 실시간 협업 (동기화, 충돌해결, 검증) + - 템플릿 관리 + - 대시보드 + version: 1.0.0 + contact: + name: HGZero Team + email: support@hgzero.com + +servers: + - url: https://api.hgzero.com/meeting/v1 + description: Production server + - url: https://dev-api.hgzero.com/meeting/v1 + description: Development server + - url: http://localhost:8080/api + description: Local development server + +tags: + - name: Dashboard + description: 대시보드 관련 API + - name: Meeting + description: 회의 관리 API + - name: Minutes + description: 회의록 관리 API + - name: Todo + description: Todo 관리 API + - name: Collaboration + description: 실시간 협업 API + - name: Template + description: 템플릿 관리 API + - name: WebSocket + description: WebSocket 엔드포인트 + +paths: + # ==================== Dashboard APIs ==================== + /dashboard: + get: + tags: + - Dashboard + summary: 대시보드 데이터 조회 + description: | + 사용자별 맞춤 대시보드 정보를 조회합니다. + - 예정된 회의 목록 + - 진행 중 Todo 목록 + - 최근 회의록 목록 + - 공유받은 회의록 목록 + - 통계 정보 + operationId: getDashboard + x-user-story: AFR-USER-020 + x-controller: DashboardController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/UserIdHeader' + - $ref: '#/components/parameters/UserNameHeader' + - $ref: '#/components/parameters/UserEmailHeader' + responses: + '200': + description: 대시보드 데이터 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + example: + upcomingMeetings: + - meetingId: "550e8400-e29b-41d4-a716-446655440000" + title: "Q1 전략 회의" + startTime: "2025-01-25T14:00:00Z" + endTime: "2025-01-25T16:00:00Z" + location: "회의실 A" + participantCount: 5 + status: "SCHEDULED" + - meetingId: "550e8400-e29b-41d4-a716-446655440001" + title: "주간 스크럼" + startTime: "2025-01-26T10:00:00Z" + endTime: "2025-01-26T10:30:00Z" + location: "온라인" + participantCount: 8 + status: "SCHEDULED" + activeTodos: + - todoId: "660e8400-e29b-41d4-a716-446655440000" + content: "API 설계 문서 작성" + dueDate: "2025-01-30" + priority: "HIGH" + status: "IN_PROGRESS" + minutesId: "770e8400-e29b-41d4-a716-446655440000" + - todoId: "660e8400-e29b-41d4-a716-446655440001" + content: "데이터베이스 스키마 설계" + dueDate: "2025-02-05" + priority: "MEDIUM" + status: "IN_PROGRESS" + minutesId: "770e8400-e29b-41d4-a716-446655440001" + recentMinutes: + - minutesId: "770e8400-e29b-41d4-a716-446655440000" + title: "아키텍처 설계 회의" + meetingDate: "2025-01-23T14:00:00Z" + status: "FINALIZED" + participantCount: 6 + lastModified: "2025-01-23T16:30:00Z" + sharedMinutes: + - minutesId: "770e8400-e29b-41d4-a716-446655440002" + title: "프로젝트 킥오프 미팅" + meetingDate: "2025-01-22T10:00:00Z" + status: "FINALIZED" + sharedBy: "김철수" + sharedAt: "2025-01-22T15:00:00Z" + statistics: + upcomingMeetingsCount: 2 + activeTodosCount: 5 + todoCompletionRate: 68.5 + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + + # ==================== Meeting APIs ==================== + /meetings: + post: + tags: + - Meeting + summary: 회의 예약 + description: | + 새로운 회의를 예약하고 참석자를 초대합니다. + - 회의 정보 저장 + - 참석자 목록 관리 + - 초대 이메일 자동 발송 + - 리마인더 스케줄링 + operationId: createMeeting + x-user-story: UFR-MEET-010 + x-controller: MeetingController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/UserIdHeader' + - $ref: '#/components/parameters/UserNameHeader' + - $ref: '#/components/parameters/UserEmailHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMeetingRequest' + example: + title: "Q1 전략 회의" + startTime: "2025-01-25T14:00:00Z" + endTime: "2025-01-25T16:00:00Z" + location: "회의실 A" + participants: + - "user1@example.com" + - "user2@example.com" + - "user3@example.com" + responses: + '201': + description: 회의 예약 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MeetingResponse' + example: + meetingId: "550e8400-e29b-41d4-a716-446655440000" + title: "Q1 전략 회의" + startTime: "2025-01-25T14:00:00Z" + endTime: "2025-01-25T16:00:00Z" + location: "회의실 A" + participants: + - userId: "user1" + email: "user1@example.com" + name: "김철수" + role: "ORGANIZER" + - userId: "user2" + email: "user2@example.com" + name: "이영희" + role: "PARTICIPANT" + status: "SCHEDULED" + createdAt: "2025-01-23T10:00:00Z" + '400': + $ref: '#/components/responses/BadRequestError' + '409': + $ref: '#/components/responses/ConflictError' + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + + /meetings/{meetingId}/template: + put: + tags: + - Meeting + summary: 회의록 템플릿 선택 + description: | + 회의에 회의록 템플릿을 적용합니다. + - 템플릿 유형: 일반 회의, 스크럼, 프로젝트 킥오프, 주간 회의 + - 섹션 커스터마이징 가능 + operationId: applyTemplate + x-user-story: UFR-MEET-020 + x-controller: MeetingController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MeetingIdPath' + - $ref: '#/components/parameters/UserIdHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ApplyTemplateRequest' + example: + templateId: "general-meeting" + customizations: + - section: "discussion" + title: "주요 논의사항" + order: 1 + - section: "decision" + title: "결정사항" + order: 2 + responses: + '200': + description: 템플릿 적용 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MeetingResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + '401': + $ref: '#/components/responses/UnauthorizedError' + + /meetings/{meetingId}/start: + post: + tags: + - Meeting + summary: 회의 시작 + description: | + 회의를 시작하고 세션을 생성합니다. + - 회의 세션 생성 + - 음성 녹음 준비 + - 회의록 초안 생성 + - 실시간 협업 WebSocket 준비 + operationId: startMeeting + x-user-story: UFR-MEET-030 + x-controller: MeetingController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MeetingIdPath' + - $ref: '#/components/parameters/UserIdHeader' + - $ref: '#/components/parameters/UserNameHeader' + responses: + '201': + description: 회의 시작 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/SessionResponse' + example: + sessionId: "660e8400-e29b-41d4-a716-446655440000" + meetingId: "550e8400-e29b-41d4-a716-446655440000" + status: "IN_PROGRESS" + startedAt: "2025-01-25T14:00:00Z" + minutesId: "770e8400-e29b-41d4-a716-446655440000" + '404': + $ref: '#/components/responses/NotFoundError' + '409': + $ref: '#/components/responses/ConflictError' + '401': + $ref: '#/components/responses/UnauthorizedError' + + /meetings/{meetingId}/end: + post: + tags: + - Meeting + summary: 회의 종료 + description: | + 회의를 종료하고 통계를 생성합니다. + - 음성 녹음 중지 + - 회의 통계 생성 + - AI 제안 데이터 회의록에 반영 + - 참석자에게 종료 알림 + operationId: endMeeting + x-user-story: UFR-MEET-040 + x-controller: MeetingController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MeetingIdPath' + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 회의 종료 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MeetingEndResponse' + example: + meetingId: "550e8400-e29b-41d4-a716-446655440000" + sessionId: "660e8400-e29b-41d4-a716-446655440000" + status: "ENDED" + endedAt: "2025-01-25T16:00:00Z" + statistics: + duration: 120 + participantCount: 5 + utteranceCount: 87 + minutesId: "770e8400-e29b-41d4-a716-446655440000" + aiEnhancement: + merged: true + suggestionsApplied: 8 + sections: + discussion: 3 + decision: 2 + todo: 3 + '404': + $ref: '#/components/responses/NotFoundError' + '409': + $ref: '#/components/responses/ConflictError' + '401': + $ref: '#/components/responses/UnauthorizedError' + + # ==================== Minutes APIs ==================== + /minutes: + get: + tags: + - Minutes + summary: 회의록 목록 조회 + description: | + 회의록 목록을 조회하고 필터링합니다. + - 상태별 필터링 (전체/작성중/확정완료) + - 카테고리별 필터링 (전체/공유받은/참석한/생성한) + - 정렬 옵션 (최신순/회의일시순/제목순) + - 검색 기능 + operationId: getMinutesList + x-user-story: UFR-MEET-046 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/UserIdHeader' + - name: page + in: query + schema: + type: integer + default: 0 + minimum: 0 + description: 페이지 번호 (0부터 시작) + - name: size + in: query + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + description: 페이지 크기 + - name: status + in: query + schema: + type: string + enum: [ALL, DRAFT, FINALIZED] + default: ALL + description: 회의록 상태 필터 + - name: category + in: query + schema: + type: string + enum: [ALL, SHARED, ATTENDED, CREATED] + default: ALL + description: 카테고리 필터 + - name: sort + in: query + schema: + type: string + enum: [LATEST, MEETING_DATE, TITLE] + default: LATEST + description: 정렬 옵션 + - name: keyword + in: query + schema: + type: string + description: 검색 키워드 (제목, 참석자, 키워드) + responses: + '200': + description: 회의록 목록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MinutesListResponse' + example: + content: + - minutesId: "770e8400-e29b-41d4-a716-446655440000" + title: "아키텍처 설계 회의" + meetingDate: "2025-01-23T14:00:00Z" + participantCount: 6 + status: "FINALIZED" + verificationRate: 100 + isReadOnly: false + lastModified: "2025-01-23T16:30:00Z" + - minutesId: "770e8400-e29b-41d4-a716-446655440001" + title: "주간 스프린트 리뷰" + meetingDate: "2025-01-22T10:00:00Z" + participantCount: 8 + status: "DRAFT" + verificationRate: 65 + isReadOnly: true + lastModified: "2025-01-22T11:30:00Z" + pageable: + page: 0 + size: 20 + sort: "LATEST" + totalElements: 45 + totalPages: 3 + statistics: + total: 45 + draft: 12 + finalized: 33 + '401': + $ref: '#/components/responses/UnauthorizedError' + + /minutes/{minutesId}: + get: + tags: + - Minutes + summary: 회의록 상세 조회 + description: | + 회의록의 상세 정보를 조회합니다. + - 기본 정보 (제목, 일시, 참석자) + - 섹션별 상세 내용 + - AI 요약 정보 + - 관련 회의록 (최대 3개) + - 권한 정보 + operationId: getMinutesDetail + x-user-story: UFR-MEET-047 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 회의록 상세 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MinutesDetailResponse' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + '401': + $ref: '#/components/responses/UnauthorizedError' + + patch: + tags: + - Minutes + summary: 회의록 수정 + description: | + 회의록 내용을 수정합니다. + - 제목, 섹션별 내용, AI 요약 수정 가능 + - 잠긴 섹션은 수정 불가 + - 확정된 회의록은 작성중으로 상태 변경 + - 실시간 동기화 + operationId: updateMinutes + x-user-story: UFR-MEET-055 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - $ref: '#/components/parameters/UserIdHeader' + - $ref: '#/components/parameters/UserNameHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateMinutesRequest' + responses: + '200': + description: 회의록 수정 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MinutesUpdateResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + /minutes/{minutesId}/finalize: + post: + tags: + - Minutes + summary: 회의록 확정 + description: | + 최종 회의록을 확정하고 버전을 생성합니다. + - 필수 항목 검증 + - 확정 버전 생성 + - AI Todo 자동 추출 + - 참석자에게 확정 알림 + operationId: finalizeMinutes + x-user-story: UFR-MEET-050 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 회의록 확정 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/MinutesFinalizeResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '409': + $ref: '#/components/responses/ConflictError' + '404': + $ref: '#/components/responses/NotFoundError' + + /minutes/{minutesId}/sections/{sectionId}/verify: + post: + tags: + - Minutes + summary: 섹션 검증 완료 + description: | + 회의록 섹션을 검증하고 완료 표시를 합니다. + - 검증자 정보 기록 + - 검증 상태 업데이트 + - 실시간 동기화 + - 전체 메일 알림 + operationId: verifySection + x-user-story: UFR-COLLAB-030 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - name: sectionId + in: path + required: true + schema: + type: string + format: uuid + description: 섹션 ID + - $ref: '#/components/parameters/UserIdHeader' + - $ref: '#/components/parameters/UserNameHeader' + responses: + '200': + description: 섹션 검증 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/SectionVerifyResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + /minutes/{minutesId}/sections/{sectionId}/lock: + post: + tags: + - Minutes + summary: 섹션 잠금 + description: | + 검증 완료된 섹션을 잠급니다. (회의 생성자만 가능) + - 잠긴 섹션은 수정 불가 + - 실시간 동기화 + operationId: lockSection + x-user-story: UFR-COLLAB-030 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - name: sectionId + in: path + required: true + schema: + type: string + format: uuid + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 섹션 잠금 성공 + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + delete: + tags: + - Minutes + summary: 섹션 잠금 해제 + description: | + 잠긴 섹션의 잠금을 해제합니다. (회의 생성자만 가능) + operationId: unlockSection + x-user-story: UFR-MEET-055 + x-controller: MinutesController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - name: sectionId + in: path + required: true + schema: + type: string + format: uuid + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 섹션 잠금 해제 성공 + '403': + $ref: '#/components/responses/ForbiddenError' + '404': + $ref: '#/components/responses/NotFoundError' + + # ==================== Todo APIs ==================== + /todos: + post: + tags: + - Todo + summary: Todo 할당 + description: | + Todo를 생성하고 담당자에게 할당합니다. + - 회의록과 양방향 연결 + - 담당자에게 즉시 알림 발송 + - 캘린더 이벤트 생성 + - 리마인더 설정 + operationId: createTodo + x-user-story: UFR-TODO-010 + x-controller: TodoController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/UserIdHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTodoRequest' + example: + content: "API 설계 문서 작성" + assignee: "user@example.com" + dueDate: "2025-01-30" + priority: "HIGH" + minutesId: "770e8400-e29b-41d4-a716-446655440000" + sectionId: "880e8400-e29b-41d4-a716-446655440000" + responses: + '201': + description: Todo 생성 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TodoResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '404': + $ref: '#/components/responses/NotFoundError' + + /todos/{todoId}/complete: + patch: + tags: + - Todo + summary: Todo 완료 처리 + description: | + Todo를 완료 상태로 변경합니다. + - 담당자만 완료 가능 + - 회의록에 완료 상태 실시간 반영 + - 완료 알림 발송 + - 모든 Todo 완료 시 전체 완료 알림 + operationId: completeTodo + x-user-story: UFR-TODO-030 + x-controller: TodoController + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/TodoIdPath' + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: Todo 완료 처리 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TodoCompleteResponse' + '403': + $ref: '#/components/responses/ForbiddenError' + '409': + $ref: '#/components/responses/ConflictError' + '404': + $ref: '#/components/responses/NotFoundError' + + # ==================== Template APIs ==================== + /templates: + get: + tags: + - Template + summary: 템플릿 목록 조회 + description: | + 사용 가능한 회의록 템플릿 목록을 조회합니다. + - 일반 회의 + - 스크럼 회의 + - 프로젝트 킥오프 + - 주간 회의 + operationId: getTemplates + x-user-story: UFR-MEET-020 + x-controller: TemplateController + security: + - bearerAuth: [] + responses: + '200': + description: 템플릿 목록 조회 성공 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TemplateResponse' + + /templates/{templateId}: + get: + tags: + - Template + summary: 템플릿 상세 조회 + description: 특정 템플릿의 상세 정보를 조회합니다. + operationId: getTemplate + x-user-story: UFR-MEET-020 + x-controller: TemplateController + security: + - bearerAuth: [] + parameters: + - name: templateId + in: path + required: true + schema: + type: string + description: 템플릿 ID + responses: + '200': + description: 템플릿 상세 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateDetailResponse' + '404': + $ref: '#/components/responses/NotFoundError' + + # ==================== WebSocket Endpoints ==================== + /ws/minutes/{minutesId}: + get: + tags: + - WebSocket + summary: 회의록 실시간 협업 WebSocket + description: | + WebSocket 연결을 통한 실시간 협업 기능 + - 회의록 실시간 수정 동기화 + - 충돌 감지 및 해결 + - 검증 완료 상태 동기화 + - Todo 상태 변경 동기화 + + **연결 URL:** ws://localhost:8080/ws/minutes/{minutesId} + + **메시지 타입:** + - MINUTES_UPDATED: 회의록 수정 + - TODO_COMPLETED: Todo 완료 + - SECTION_VERIFIED: 섹션 검증 완료 + - CONFLICT_DETECTED: 충돌 감지 + operationId: connectMinutesWebSocket + x-user-story: UFR-COLLAB-010 + x-controller: CollaborationController + parameters: + - $ref: '#/components/parameters/MinutesIdPath' + - name: userId + in: query + required: true + schema: + type: string + description: 사용자 ID + - name: token + in: query + required: true + schema: + type: string + description: JWT 토큰 + responses: + '101': + description: WebSocket 연결 성공 + '401': + description: 인증 실패 + '403': + description: 권한 없음 + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰을 Authorization 헤더에 포함 + + parameters: + UserIdHeader: + name: X-User-Id + in: header + required: true + schema: + type: string + description: 사용자 ID + + UserNameHeader: + name: X-User-Name + in: header + required: true + schema: + type: string + description: 사용자 이름 + + UserEmailHeader: + name: X-User-Email + in: header + required: true + schema: + type: string + format: email + description: 사용자 이메일 + + MeetingIdPath: + name: meetingId + in: path + required: true + schema: + type: string + format: uuid + description: 회의 ID + + MinutesIdPath: + name: minutesId + in: path + required: true + schema: + type: string + format: uuid + description: 회의록 ID + + TodoIdPath: + name: todoId + in: path + required: true + schema: + type: string + format: uuid + description: Todo ID + + schemas: + # ==================== Dashboard Schemas ==================== + DashboardResponse: + type: object + properties: + upcomingMeetings: + type: array + items: + $ref: '#/components/schemas/UpcomingMeeting' + description: 예정된 회의 목록 + activeTodos: + type: array + items: + $ref: '#/components/schemas/ActiveTodo' + description: 진행 중 Todo 목록 + recentMinutes: + type: array + items: + $ref: '#/components/schemas/RecentMinutes' + description: 최근 회의록 목록 + sharedMinutes: + type: array + items: + $ref: '#/components/schemas/SharedMinutes' + description: 공유받은 회의록 목록 + statistics: + $ref: '#/components/schemas/DashboardStatistics' + description: 통계 정보 + required: + - statistics + + UpcomingMeeting: + type: object + properties: + meetingId: + type: string + format: uuid + title: + type: string + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + location: + type: string + participantCount: + type: integer + status: + type: string + enum: [SCHEDULED, IN_PROGRESS] + required: + - meetingId + - title + - startTime + - status + + ActiveTodo: + type: object + properties: + todoId: + type: string + format: uuid + content: + type: string + dueDate: + type: string + format: date + priority: + type: string + enum: [HIGH, MEDIUM, LOW] + status: + type: string + enum: [IN_PROGRESS] + minutesId: + type: string + format: uuid + required: + - todoId + - content + - priority + - status + + RecentMinutes: + type: object + properties: + minutesId: + type: string + format: uuid + title: + type: string + meetingDate: + type: string + format: date-time + status: + type: string + enum: [DRAFT, FINALIZED] + participantCount: + type: integer + lastModified: + type: string + format: date-time + required: + - minutesId + - title + - meetingDate + - status + + SharedMinutes: + type: object + properties: + minutesId: + type: string + format: uuid + title: + type: string + meetingDate: + type: string + format: date-time + status: + type: string + enum: [DRAFT, FINALIZED] + sharedBy: + type: string + sharedAt: + type: string + format: date-time + required: + - minutesId + - title + - status + + DashboardStatistics: + type: object + properties: + upcomingMeetingsCount: + type: integer + activeTodosCount: + type: integer + todoCompletionRate: + type: number + format: double + description: Todo 완료율 (%) + required: + - upcomingMeetingsCount + - activeTodosCount + - todoCompletionRate + + # ==================== Meeting Schemas ==================== + CreateMeetingRequest: + type: object + properties: + title: + type: string + maxLength: 100 + description: 회의 제목 + startTime: + type: string + format: date-time + description: 회의 시작 시간 + endTime: + type: string + format: date-time + description: 회의 종료 시간 + location: + type: string + maxLength: 200 + description: 회의 장소 + participants: + type: array + items: + type: string + format: email + minItems: 1 + description: 참석자 이메일 목록 + required: + - title + - startTime + - endTime + - participants + + MeetingResponse: + type: object + properties: + meetingId: + type: string + format: uuid + title: + type: string + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + location: + type: string + participants: + type: array + items: + $ref: '#/components/schemas/Participant' + status: + type: string + enum: [SCHEDULED, IN_PROGRESS, ENDED] + createdAt: + type: string + format: date-time + required: + - meetingId + - title + - startTime + - endTime + - participants + - status + + Participant: + type: object + properties: + userId: + type: string + email: + type: string + format: email + name: + type: string + role: + type: string + enum: [ORGANIZER, PARTICIPANT] + required: + - userId + - email + - name + - role + + ApplyTemplateRequest: + type: object + properties: + templateId: + type: string + description: 템플릿 ID + customizations: + type: array + items: + $ref: '#/components/schemas/SectionCustomization' + required: + - templateId + + SectionCustomization: + type: object + properties: + section: + type: string + title: + type: string + order: + type: integer + required: + - section + - title + - order + + SessionResponse: + type: object + properties: + sessionId: + type: string + format: uuid + meetingId: + type: string + format: uuid + status: + type: string + enum: [IN_PROGRESS] + startedAt: + type: string + format: date-time + minutesId: + type: string + format: uuid + required: + - sessionId + - meetingId + - status + - startedAt + - minutesId + + MeetingEndResponse: + type: object + properties: + meetingId: + type: string + format: uuid + sessionId: + type: string + format: uuid + status: + type: string + enum: [ENDED] + endedAt: + type: string + format: date-time + statistics: + $ref: '#/components/schemas/MeetingStatistics' + minutesId: + type: string + format: uuid + aiEnhancement: + $ref: '#/components/schemas/AIEnhancement' + required: + - meetingId + - sessionId + - status + - endedAt + - statistics + - minutesId + + MeetingStatistics: + type: object + properties: + duration: + type: integer + description: 회의 진행 시간 (분) + participantCount: + type: integer + description: 참석자 수 + utteranceCount: + type: integer + description: 발언 횟수 + required: + - duration + - participantCount + - utteranceCount + + AIEnhancement: + type: object + properties: + merged: + type: boolean + description: AI 제안 병합 여부 + suggestionsApplied: + type: integer + description: 적용된 AI 제안 수 + sections: + type: object + properties: + discussion: + type: integer + decision: + type: integer + todo: + type: integer + required: + - merged + - suggestionsApplied + + # ==================== Minutes Schemas ==================== + MinutesListResponse: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/MinutesSummary' + pageable: + $ref: '#/components/schemas/Pageable' + totalElements: + type: integer + totalPages: + type: integer + statistics: + $ref: '#/components/schemas/MinutesStatistics' + required: + - content + - pageable + - totalElements + - totalPages + - statistics + + MinutesSummary: + type: object + properties: + minutesId: + type: string + format: uuid + title: + type: string + meetingDate: + type: string + format: date-time + participantCount: + type: integer + status: + type: string + enum: [DRAFT, FINALIZED] + verificationRate: + type: integer + description: 검증 완료율 (%) + isReadOnly: + type: boolean + description: 조회 전용 여부 + lastModified: + type: string + format: date-time + required: + - minutesId + - title + - meetingDate + - status + + Pageable: + type: object + properties: + page: + type: integer + size: + type: integer + sort: + type: string + required: + - page + - size + - sort + + MinutesStatistics: + type: object + properties: + total: + type: integer + draft: + type: integer + finalized: + type: integer + required: + - total + - draft + - finalized + + MinutesDetailResponse: + type: object + properties: + minutesId: + type: string + format: uuid + meeting: + $ref: '#/components/schemas/MeetingInfo' + participants: + type: array + items: + $ref: '#/components/schemas/Participant' + sections: + type: array + items: + $ref: '#/components/schemas/MinutesSection' + aiSummaries: + type: array + items: + $ref: '#/components/schemas/AISummary' + relatedMinutes: + type: array + items: + $ref: '#/components/schemas/RelatedMinutes' + permissions: + $ref: '#/components/schemas/MinutesPermissions' + status: + type: string + enum: [DRAFT, FINALIZED] + version: + type: string + required: + - minutesId + - meeting + - participants + - sections + - permissions + - status + + MeetingInfo: + type: object + properties: + meetingId: + type: string + format: uuid + title: + type: string + startTime: + type: string + format: date-time + endTime: + type: string + format: date-time + location: + type: string + duration: + type: integer + description: 소요 시간 (분) + required: + - meetingId + - title + - startTime + + MinutesSection: + type: object + properties: + sectionId: + type: string + format: uuid + sectionType: + type: string + enum: [DISCUSSION, DECISION, TODO, SCHEDULE, RESOURCE, CUSTOM] + title: + type: string + content: + type: string + order: + type: integer + isLocked: + type: boolean + isVerified: + type: boolean + verifiedBy: + type: string + verifiedAt: + type: string + format: date-time + required: + - sectionId + - sectionType + - title + - content + - order + - isLocked + - isVerified + + AISummary: + type: object + properties: + summaryId: + type: string + format: uuid + sectionId: + type: string + format: uuid + content: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + required: + - summaryId + - sectionId + - content + + RelatedMinutes: + type: object + properties: + minutesId: + type: string + format: uuid + title: + type: string + meetingDate: + type: string + format: date-time + relevanceScore: + type: number + format: double + description: 관련도 점수 (0-100%) + summary: + type: string + required: + - minutesId + - title + - meetingDate + - relevanceScore + + MinutesPermissions: + type: object + properties: + canEdit: + type: boolean + canShare: + type: boolean + required: + - canEdit + - canShare + + UpdateMinutesRequest: + type: object + properties: + title: + type: string + sections: + type: array + items: + $ref: '#/components/schemas/SectionUpdate' + + SectionUpdate: + type: object + properties: + sectionId: + type: string + format: uuid + content: + type: string + aiSummary: + type: string + required: + - sectionId + + MinutesUpdateResponse: + type: object + properties: + minutesId: + type: string + format: uuid + status: + type: string + enum: [DRAFT, FINALIZED] + updatedAt: + type: string + format: date-time + updatedBy: + type: string + version: + type: string + required: + - minutesId + - status + - updatedAt + - updatedBy + + MinutesFinalizeResponse: + type: object + properties: + minutesId: + type: string + format: uuid + status: + type: string + enum: [FINALIZED] + version: + type: string + finalizedAt: + type: string + format: date-time + finalizedBy: + type: string + required: + - minutesId + - status + - version + - finalizedAt + - finalizedBy + + SectionVerifyResponse: + type: object + properties: + sectionId: + type: string + format: uuid + isVerified: + type: boolean + verifiedBy: + type: string + verifiedAt: + type: string + format: date-time + required: + - sectionId + - isVerified + + # ==================== Todo Schemas ==================== + CreateTodoRequest: + type: object + properties: + content: + type: string + maxLength: 500 + assignee: + type: string + format: email + dueDate: + type: string + format: date + priority: + type: string + enum: [HIGH, MEDIUM, LOW] + default: MEDIUM + minutesId: + type: string + format: uuid + sectionId: + type: string + format: uuid + required: + - content + - assignee + - minutesId + + TodoResponse: + type: object + properties: + todoId: + type: string + format: uuid + content: + type: string + assignee: + type: string + format: email + dueDate: + type: string + format: date + priority: + type: string + enum: [HIGH, MEDIUM, LOW] + status: + type: string + enum: [IN_PROGRESS, COMPLETED] + minutesId: + type: string + format: uuid + sectionId: + type: string + format: uuid + calendarEventId: + type: string + createdAt: + type: string + format: date-time + required: + - todoId + - content + - assignee + - priority + - status + - minutesId + + TodoCompleteResponse: + type: object + properties: + todoId: + type: string + format: uuid + status: + type: string + enum: [COMPLETED] + completedAt: + type: string + format: date-time + completedBy: + type: string + minutesId: + type: string + format: uuid + allTodosCompleted: + type: boolean + required: + - todoId + - status + - completedAt + - completedBy + - allTodosCompleted + + # ==================== Template Schemas ==================== + TemplateResponse: + type: object + properties: + templateId: + type: string + name: + type: string + description: + type: string + category: + type: string + enum: [GENERAL, SCRUM, KICKOFF, WEEKLY] + required: + - templateId + - name + - category + + TemplateDetailResponse: + type: object + properties: + templateId: + type: string + name: + type: string + description: + type: string + category: + type: string + sections: + type: array + items: + $ref: '#/components/schemas/TemplateSection' + required: + - templateId + - name + - category + - sections + + TemplateSection: + type: object + properties: + sectionType: + type: string + title: + type: string + order: + type: integer + isRequired: + type: boolean + required: + - sectionType + - title + - order + - isRequired + + # ==================== Error Schemas ==================== + ErrorResponse: + type: object + properties: + error: + $ref: '#/components/schemas/Error' + required: + - error + + Error: + type: object + properties: + code: + type: string + description: 에러 코드 + message: + type: string + description: 에러 메시지 + details: + type: string + description: 상세 설명 + timestamp: + type: string + format: date-time + description: 발생 시간 + path: + type: string + description: 요청 경로 + required: + - code + - message + - timestamp + + responses: + BadRequestError: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "BAD_REQUEST" + message: "잘못된 요청입니다" + details: "요청 데이터의 형식이 올바르지 않습니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/meetings" + + UnauthorizedError: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "UNAUTHORIZED" + message: "인증이 필요합니다" + details: "유효한 JWT 토큰을 제공해주세요" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/dashboard" + + ForbiddenError: + description: 권한 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "FORBIDDEN" + message: "권한이 없습니다" + details: "해당 리소스에 접근할 권한이 없습니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/minutes/{minutesId}" + + NotFoundError: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "NOT_FOUND" + message: "리소스를 찾을 수 없습니다" + details: "요청한 리소스가 존재하지 않습니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/meetings/{meetingId}" + + ConflictError: + description: 충돌 발생 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "CONFLICT" + message: "충돌이 발생했습니다" + details: "중복된 회의 시간이 존재합니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/meetings" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: + code: "INTERNAL_SERVER_ERROR" + message: "서버 내부 오류가 발생했습니다" + details: "잠시 후 다시 시도해주세요" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/dashboard" + +security: + - bearerAuth: [] diff --git a/design/backend/api/notification-service-api.yaml b/design/backend/api/notification-service-api.yaml new file mode 100644 index 0000000..2150dc4 --- /dev/null +++ b/design/backend/api/notification-service-api.yaml @@ -0,0 +1,932 @@ +openapi: 3.0.3 +info: + title: Notification Service API + description: | + 회의록 작성 및 공유 개선 서비스의 알림 발송 및 리마인더 관리 API + + ## 주요 기능 + - 이메일 알림 발송 (회의 초대, Todo 할당, 리마인더) + - 알림 이력 조회 및 관리 + - 사용자 알림 설정 관리 + + ## Event-Driven Architecture + - 알림 발송은 Azure Event Hubs를 통한 이벤트 기반 처리 + - 비동기 처리로 성능 최적화 + - 재시도 메커니즘으로 신뢰성 보장 + version: 1.0.0 + contact: + name: Backend Development Team + email: backend@example.com + +servers: + - url: https://api.hgzero.com/notification/v1 + description: Production Server + - url: https://dev-api.hgzero.com/notification/v1 + description: Development Server + - url: http://localhost:8083/api/v1 + description: Local Development + +tags: + - name: Notification + description: 알림 발송 및 관리 API + - name: Notification Settings + description: 사용자 알림 설정 API + - name: Internal + description: 내부 서비스 간 통신 API (Event Handler) + +paths: + # ======================================== + # Notification APIs + # ======================================== + + /notifications/invitation: + post: + summary: 회의 초대 알림 발송 (내부 API) + description: | + MeetingCreated 이벤트를 수신하여 참석자에게 회의 초대 이메일을 발송합니다. + - Event-Driven 방식으로 Meeting Service에서 이벤트 발행 + - 참석자별 병렬 이메일 발송 + - 발송 이력 저장 및 재시도 메커니즘 + operationId: sendMeetingInvitation + x-user-story: UFR-MEET-010 + x-controller: NotificationController + tags: + - Internal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MeetingInvitationRequest' + example: + meetingId: "MTG-20250123-001" + title: "2025년 1분기 전략 회의" + scheduledAt: "2025-01-30T14:00:00Z" + location: "회의실 A" + participants: + - userId: "U001" + userName: "김철수" + email: "kim@example.com" + - userId: "U002" + userName: "이영희" + email: "lee@example.com" + creatorName: "박팀장" + meetingLink: "https://meet.hgzero.com/MTG-20250123-001" + calendarLink: "https://calendar.hgzero.com/add/MTG-20250123-001" + responses: + '200': + description: 알림 발송 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationResponse' + example: + notificationId: "NTF-20250123-001" + status: "SENT" + sentCount: 2 + failedCount: 0 + createdAt: "2025-01-23T10:00:00Z" + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /notifications/todo: + post: + summary: Todo 할당 알림 발송 (내부 API) + description: | + TodoAssigned 이벤트를 수신하여 담당자에게 Todo 알림 이메일을 발송합니다. + - 할당 즉시 알림 + - 마감일 3일 전, 1일 전, 당일 리마인더 스케줄링 + - 회의록 링크 포함 + operationId: sendTodoNotification + x-user-story: UFR-TODO-010 + x-controller: NotificationController + tags: + - Internal + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TodoNotificationRequest' + example: + todoId: "TODO-20250123-001" + meetingId: "MTG-20250123-001" + assignee: + userId: "U001" + userName: "김철수" + email: "kim@example.com" + content: "API 설계서 작성 및 검토" + dueDate: "2025-01-30T23:59:59Z" + priority: "HIGH" + minutesLink: "https://hgzero.com/minutes/MTG-20250123-001#section-3" + meetingTitle: "2025년 1분기 전략 회의" + responses: + '200': + description: 알림 발송 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationResponse' + example: + notificationId: "NTF-20250123-002" + status: "SENT" + sentCount: 1 + failedCount: 0 + createdAt: "2025-01-23T10:05:00Z" + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /notifications: + get: + summary: 사용자별 알림 이력 조회 + description: | + 사용자가 받은 알림 이력을 조회합니다. + - 알림 유형별 필터링 (초대, Todo, 리마인더) + - 상태별 필터링 (발송완료, 발송실패) + - 페이징 지원 + operationId: getNotifications + x-user-story: AFR-USER-020 + x-controller: NotificationController + tags: + - Notification + parameters: + - $ref: '#/components/parameters/UserIdHeader' + - name: type + in: query + description: 알림 유형 필터 + required: false + schema: + type: string + enum: + - INVITATION + - TODO_ASSIGNED + - REMINDER + - TODO_REMINDER + - name: status + in: query + description: 알림 상태 필터 + required: false + schema: + type: string + enum: + - SENT + - FAILED + - PENDING + - name: page + in: query + description: 페이지 번호 (0부터 시작) + required: false + schema: + type: integer + minimum: 0 + default: 0 + - name: size + in: query + description: 페이지당 항목 수 + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + responses: + '200': + description: 알림 이력 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationListResponse' + example: + notifications: + - notificationId: "NTF-20250123-002" + type: "TODO_ASSIGNED" + title: "[TODO 할당] API 설계서 작성 및 검토" + content: "Todo가 할당되었습니다. 마감일: 2025-01-30" + status: "SENT" + sentAt: "2025-01-23T10:05:00Z" + relatedId: "TODO-20250123-001" + - notificationId: "NTF-20250123-001" + type: "INVITATION" + title: "[회의 초대] 2025년 1분기 전략 회의" + content: "회의에 초대되었습니다. 일시: 2025-01-30 14:00" + status: "SENT" + sentAt: "2025-01-23T10:00:00Z" + relatedId: "MTG-20250123-001" + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 2 + totalPages: 1 + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /notifications/{notificationId}: + get: + summary: 특정 알림 상세 조회 + description: | + 알림 ID로 특정 알림의 상세 정보를 조회합니다. + - 발송 상태 + - 수신자 정보 + - 템플릿 정보 + - 재시도 이력 + operationId: getNotificationById + x-user-story: AFR-USER-020 + x-controller: NotificationController + tags: + - Notification + parameters: + - $ref: '#/components/parameters/UserIdHeader' + - name: notificationId + in: path + description: 알림 ID + required: true + schema: + type: string + pattern: '^NTF-[0-9]{8}-[0-9]{3}$' + example: "NTF-20250123-001" + responses: + '200': + description: 알림 상세 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationDetailResponse' + example: + notificationId: "NTF-20250123-001" + type: "INVITATION" + title: "[회의 초대] 2025년 1분기 전략 회의" + content: "회의에 초대되었습니다." + status: "SENT" + relatedId: "MTG-20250123-001" + recipients: + - email: "kim@example.com" + status: "SENT" + sentAt: "2025-01-23T10:00:05Z" + - email: "lee@example.com" + status: "SENT" + sentAt: "2025-01-23T10:00:06Z" + templateId: "meeting-invitation" + createdAt: "2025-01-23T10:00:00Z" + completedAt: "2025-01-23T10:00:10Z" + retryCount: 0 + '404': + $ref: '#/components/responses/NotFound' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + # ======================================== + # Notification Settings APIs + # ======================================== + + /notifications/settings: + get: + summary: 사용자 알림 설정 조회 + description: | + 사용자의 알림 설정을 조회합니다. + - 알림 채널 설정 (이메일/SMS) + - 알림 유형별 활성화 여부 + - 수신 시간대 설정 + operationId: getNotificationSettings + x-user-story: AFR-USER-020 + x-controller: NotificationSettingsController + tags: + - Notification Settings + parameters: + - $ref: '#/components/parameters/UserIdHeader' + responses: + '200': + description: 알림 설정 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSettingsResponse' + example: + userId: "U001" + channels: + email: true + sms: false + preferences: + invitationEnabled: true + todoEnabled: true + reminderEnabled: true + minutesUpdateEnabled: false + quietHours: + enabled: true + startTime: "22:00" + endTime: "08:00" + timezone: "Asia/Seoul" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + put: + summary: 사용자 알림 설정 업데이트 + description: | + 사용자의 알림 설정을 변경합니다. + - 부분 업데이트 지원 + - 설정 변경 즉시 적용 + operationId: updateNotificationSettings + x-user-story: AFR-USER-020 + x-controller: NotificationSettingsController + tags: + - Notification Settings + parameters: + - $ref: '#/components/parameters/UserIdHeader' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSettingsUpdateRequest' + example: + channels: + email: true + sms: false + preferences: + invitationEnabled: true + todoEnabled: true + reminderEnabled: false + minutesUpdateEnabled: false + quietHours: + enabled: true + startTime: "23:00" + endTime: "07:00" + timezone: "Asia/Seoul" + responses: + '200': + description: 알림 설정 업데이트 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSettingsResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + +# ======================================== +# Components +# ======================================== + +components: + schemas: + # Request Schemas + MeetingInvitationRequest: + type: object + required: + - meetingId + - title + - scheduledAt + - participants + - creatorName + properties: + meetingId: + type: string + description: 회의 ID + pattern: '^MTG-[0-9]{8}-[0-9]{3}$' + example: "MTG-20250123-001" + title: + type: string + description: 회의 제목 + maxLength: 100 + example: "2025년 1분기 전략 회의" + scheduledAt: + type: string + format: date-time + description: 회의 시작 일시 (ISO 8601) + example: "2025-01-30T14:00:00Z" + location: + type: string + description: 회의 장소 + maxLength: 200 + example: "회의실 A" + participants: + type: array + description: 참석자 목록 + minItems: 1 + items: + $ref: '#/components/schemas/Participant' + creatorName: + type: string + description: 회의 생성자 이름 + example: "박팀장" + meetingLink: + type: string + format: uri + description: 회의 참여 링크 + example: "https://meet.hgzero.com/MTG-20250123-001" + calendarLink: + type: string + format: uri + description: 캘린더 추가 링크 + example: "https://calendar.hgzero.com/add/MTG-20250123-001" + + TodoNotificationRequest: + type: object + required: + - todoId + - meetingId + - assignee + - content + - dueDate + - priority + - minutesLink + - meetingTitle + properties: + todoId: + type: string + description: Todo ID + pattern: '^TODO-[0-9]{8}-[0-9]{3}$' + example: "TODO-20250123-001" + meetingId: + type: string + description: 관련 회의 ID + pattern: '^MTG-[0-9]{8}-[0-9]{3}$' + example: "MTG-20250123-001" + assignee: + $ref: '#/components/schemas/Participant' + content: + type: string + description: Todo 내용 + maxLength: 500 + example: "API 설계서 작성 및 검토" + dueDate: + type: string + format: date-time + description: 마감일 (ISO 8601) + example: "2025-01-30T23:59:59Z" + priority: + type: string + description: 우선순위 + enum: + - HIGH + - MEDIUM + - LOW + example: "HIGH" + minutesLink: + type: string + format: uri + description: 회의록 링크 (해당 섹션) + example: "https://hgzero.com/minutes/MTG-20250123-001#section-3" + meetingTitle: + type: string + description: 회의 제목 + example: "2025년 1분기 전략 회의" + + NotificationSettingsUpdateRequest: + type: object + properties: + channels: + type: object + description: 알림 채널 설정 + properties: + email: + type: boolean + description: 이메일 알림 활성화 + sms: + type: boolean + description: SMS 알림 활성화 + preferences: + type: object + description: 알림 유형별 활성화 설정 + properties: + invitationEnabled: + type: boolean + description: 회의 초대 알림 + todoEnabled: + type: boolean + description: Todo 할당 알림 + reminderEnabled: + type: boolean + description: 리마인더 알림 + minutesUpdateEnabled: + type: boolean + description: 회의록 수정 알림 + quietHours: + type: object + description: 방해 금지 시간대 설정 + properties: + enabled: + type: boolean + description: 방해 금지 시간대 활성화 + startTime: + type: string + format: time + description: 시작 시간 (HH:mm) + example: "22:00" + endTime: + type: string + format: time + description: 종료 시간 (HH:mm) + example: "08:00" + timezone: + type: string + description: 타임존 + example: "Asia/Seoul" + + # Response Schemas + NotificationResponse: + type: object + required: + - notificationId + - status + - sentCount + - failedCount + - createdAt + properties: + notificationId: + type: string + description: 알림 ID + pattern: '^NTF-[0-9]{8}-[0-9]{3}$' + example: "NTF-20250123-001" + status: + type: string + description: 알림 전체 상태 + enum: + - SENT + - PARTIAL + - FAILED + - PENDING + example: "SENT" + sentCount: + type: integer + description: 발송 성공 건수 + minimum: 0 + example: 2 + failedCount: + type: integer + description: 발송 실패 건수 + minimum: 0 + example: 0 + createdAt: + type: string + format: date-time + description: 알림 생성 일시 + example: "2025-01-23T10:00:00Z" + + NotificationListResponse: + type: object + required: + - notifications + - pagination + properties: + notifications: + type: array + items: + $ref: '#/components/schemas/NotificationSummary' + pagination: + $ref: '#/components/schemas/PaginationInfo' + + NotificationSummary: + type: object + required: + - notificationId + - type + - title + - status + - sentAt + properties: + notificationId: + type: string + description: 알림 ID + example: "NTF-20250123-001" + type: + type: string + description: 알림 유형 + enum: + - INVITATION + - TODO_ASSIGNED + - REMINDER + - TODO_REMINDER + example: "INVITATION" + title: + type: string + description: 알림 제목 + example: "[회의 초대] 2025년 1분기 전략 회의" + content: + type: string + description: 알림 내용 요약 + example: "회의에 초대되었습니다. 일시: 2025-01-30 14:00" + status: + type: string + description: 발송 상태 + enum: + - SENT + - FAILED + - PENDING + example: "SENT" + sentAt: + type: string + format: date-time + description: 발송 일시 + example: "2025-01-23T10:00:00Z" + relatedId: + type: string + description: 관련 객체 ID (회의 ID, Todo ID 등) + example: "MTG-20250123-001" + + NotificationDetailResponse: + type: object + required: + - notificationId + - type + - title + - content + - status + - relatedId + - recipients + - createdAt + properties: + notificationId: + type: string + description: 알림 ID + example: "NTF-20250123-001" + type: + type: string + description: 알림 유형 + enum: + - INVITATION + - TODO_ASSIGNED + - REMINDER + - TODO_REMINDER + title: + type: string + description: 알림 제목 + example: "[회의 초대] 2025년 1분기 전략 회의" + content: + type: string + description: 알림 전체 내용 + example: "회의에 초대되었습니다." + status: + type: string + description: 알림 전체 상태 + enum: + - SENT + - PARTIAL + - FAILED + - PENDING + relatedId: + type: string + description: 관련 객체 ID + example: "MTG-20250123-001" + recipients: + type: array + description: 수신자별 발송 상태 + items: + type: object + properties: + email: + type: string + format: email + description: 수신자 이메일 + status: + type: string + enum: + - SENT + - FAILED + - PENDING + sentAt: + type: string + format: date-time + description: 발송 일시 + errorMessage: + type: string + description: 오류 메시지 (실패 시) + templateId: + type: string + description: 사용된 템플릿 ID + example: "meeting-invitation" + createdAt: + type: string + format: date-time + description: 알림 생성 일시 + completedAt: + type: string + format: date-time + description: 알림 완료 일시 + retryCount: + type: integer + description: 재시도 횟수 + minimum: 0 + + NotificationSettingsResponse: + type: object + required: + - userId + - channels + - preferences + properties: + userId: + type: string + description: 사용자 ID + example: "U001" + channels: + type: object + description: 알림 채널 설정 + required: + - email + - sms + properties: + email: + type: boolean + description: 이메일 알림 활성화 + sms: + type: boolean + description: SMS 알림 활성화 + preferences: + type: object + description: 알림 유형별 활성화 설정 + required: + - invitationEnabled + - todoEnabled + - reminderEnabled + - minutesUpdateEnabled + properties: + invitationEnabled: + type: boolean + description: 회의 초대 알림 + todoEnabled: + type: boolean + description: Todo 할당 알림 + reminderEnabled: + type: boolean + description: 리마인더 알림 + minutesUpdateEnabled: + type: boolean + description: 회의록 수정 알림 + quietHours: + type: object + description: 방해 금지 시간대 설정 + properties: + enabled: + type: boolean + startTime: + type: string + format: time + endTime: + type: string + format: time + timezone: + type: string + + # Common Schemas + Participant: + type: object + required: + - userId + - userName + - email + properties: + userId: + type: string + description: 사용자 ID + example: "U001" + userName: + type: string + description: 사용자 이름 + example: "김철수" + email: + type: string + format: email + description: 이메일 주소 + example: "kim@example.com" + + PaginationInfo: + type: object + required: + - currentPage + - pageSize + - totalElements + - totalPages + properties: + currentPage: + type: integer + description: 현재 페이지 번호 (0부터 시작) + minimum: 0 + example: 0 + pageSize: + type: integer + description: 페이지당 항목 수 + minimum: 1 + example: 20 + totalElements: + type: integer + description: 전체 항목 수 + minimum: 0 + example: 2 + totalPages: + type: integer + description: 전체 페이지 수 + minimum: 0 + example: 1 + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 오류 코드 + example: "INVALID_REQUEST" + message: + type: string + description: 오류 메시지 + example: "Invalid request parameters" + timestamp: + type: string + format: date-time + description: 오류 발생 시각 + details: + type: array + description: 상세 오류 정보 + items: + type: object + properties: + field: + type: string + message: + type: string + + parameters: + UserIdHeader: + name: X-User-Id + in: header + description: 사용자 ID (JWT 토큰에서 추출) + required: true + schema: + type: string + example: "U001" + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INVALID_REQUEST" + message: "Invalid request parameters" + timestamp: "2025-01-23T10:00:00Z" + details: + - field: "meetingId" + message: "Meeting ID is required" + + Unauthorized: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "UNAUTHORIZED" + message: "Authentication required" + timestamp: "2025-01-23T10:00:00Z" + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "NOT_FOUND" + message: "Notification not found" + timestamp: "2025-01-23T10:00:00Z" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INTERNAL_SERVER_ERROR" + message: "An unexpected error occurred" + timestamp: "2025-01-23T10:00:00Z" + + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증 + +security: + - BearerAuth: [] diff --git a/design/backend/api/spec/ai-service-api-spec.md b/design/backend/api/spec/ai-service-api-spec.md new file mode 100644 index 0000000..697f01d --- /dev/null +++ b/design/backend/api/spec/ai-service-api-spec.md @@ -0,0 +1,458 @@ +# AI Service API 설계서 + +## 개요 + +AI Service는 회의록 작성 및 공유 개선 서비스의 핵심 차별화 기능을 제공하는 마이크로서비스입니다. + +### 주요 기능 +- **회의록 자동 작성**: LLM 기반 회의록 자동 생성 +- **Todo 자동 추출**: 액션 아이템 자동 식별 및 담당자 추출 +- **회의록 개선**: 프롬프팅 기반 다양한 형식 변환 +- **관련 회의록 연결**: RAG 기반 벡터 유사도 검색 +- **전문용어 감지**: 맥락 기반 용어 설명 제공 +- **실시간 추천**: 논의사항/결정사항 자동 제안 + +## API 명세 파일 +- **파일 위치**: `design/backend/api/ai-service-api.yaml` +- **OpenAPI 버전**: 3.0.3 +- **검증 상태**: ✅ Valid + +## API 엔드포인트 목록 + +### 1. 회의록 자동 작성 (Transcript) + +#### POST /transcripts/process +회의록 자동 작성 + +**유저스토리**: UFR-AI-010 +**Controller**: TranscriptController + +**Request Body**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "transcriptText": "안녕하세요. 오늘 회의는...", + "userId": "user123", + "userName": "김철수", + "context": { + "title": "신규 프로젝트 킥오프 미팅", + "participants": ["김철수", "이영희", "박민수"], + "agenda": ["프로젝트 개요", "일정 논의", "역할 분담"] + } +} +``` + +**Response (200 OK)**: +```json +{ + "transcriptId": "660e8400-e29b-41d4-a716-446655440001", + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "content": { + "summary": "프로젝트 킥오프 미팅에서 기술 스택과 일정을 확정했습니다.", + "discussions": [...], + "decisions": [...], + "pendingItems": [...] + }, + "suggestions": { + "discussionTopics": [...], + "decisions": [...] + }, + "createdAt": "2025-01-23T10:30:00Z", + "status": "DRAFT" +} +``` + +--- + +### 2. Todo 자동 추출 (Todo) + +#### POST /todos/extract +Todo 자동 추출 + +**유저스토리**: UFR-AI-020 +**Controller**: TodoController + +**Request Body**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "userId": "user123" +} +``` + +**Response (200 OK)**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "todos": [ + { + "content": "API 설계서 작성", + "assignee": "박민수", + "dueDate": "2025-01-30", + "priority": "HIGH", + "sectionReference": "결정사항 #3" + } + ], + "totalCount": 5, + "extractedAt": "2025-01-23T11:00:00Z" +} +``` + +--- + +### 3. 회의록 개선 (Improve) + +#### POST /transcripts/{meetingId}/improve +회의록 개선 + +**유저스토리**: UFR-AI-030 +**Controller**: ImproveController + +**Path Parameters**: +- `meetingId` (uuid, required): 회의 ID + +**Request Body**: +```json +{ + "promptType": "1PAGE_SUMMARY", + "customPrompt": "경영진 보고용으로 3가지 핵심 결정사항만 요약해주세요", + "userId": "user123" +} +``` + +**Prompt Types**: +- `1PAGE_SUMMARY`: A4 1장 분량 요약 +- `CORE_SUMMARY`: 3-5개 핵심 포인트 +- `DETAILED_REPORT`: 시간순 상세 기록 +- `DECISION_FOCUSED`: 의사결정 중심 +- `ACTION_FOCUSED`: 액션 아이템 중심 +- `EXECUTIVE_REPORT`: 경영진 보고용 +- `CUSTOM`: 사용자 정의 + +**Response (200 OK)**: +```json +{ + "transcriptId": "770e8400-e29b-41d4-a716-446655440002", + "version": 2, + "baseVersion": 1, + "improvementType": "1PAGE_SUMMARY", + "content": "## 프로젝트 킥오프 미팅 요약\n\n### 핵심 결정사항...", + "originalLink": "/transcripts/660e8400-e29b-41d4-a716-446655440001", + "createdAt": "2025-01-23T11:30:00Z" +} +``` + +--- + +### 4. 관련 회의록 연결 (Relation) + +#### GET /transcripts/{meetingId}/related +관련 회의록 조회 + +**유저스토리**: UFR-AI-040 +**Controller**: RelationController + +**Path Parameters**: +- `meetingId` (uuid, required): 회의 ID + +**Query Parameters**: +- `transcriptId` (uuid, required): 회의록 ID +- `limit` (integer, optional): 최대 개수 (default: 5, max: 10) + +**Response (200 OK)**: +```json +{ + "relatedTranscripts": [ + { + "transcriptId": "aa0e8400-e29b-41d4-a716-446655440005", + "title": "프로젝트 X 주간 회의", + "date": "2025-01-15", + "participants": ["김철수", "이영희"], + "relevanceScore": 85.5, + "commonKeywords": ["MSA", "API Gateway", "Spring Boot"], + "link": "/transcripts/aa0e8400-e29b-41d4-a716-446655440005" + } + ], + "totalCount": 3 +} +``` + +--- + +### 5. 전문용어 감지 및 설명 (Term) + +#### POST /terms/detect +전문용어 감지 + +**유저스토리**: UFR-RAG-010 +**Controller**: TermController + +**Request Body**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "text": "MSA 아키텍처로 설계하고, API Gateway를 통해 라우팅합니다...", + "organizationId": "org123" +} +``` + +**Response (200 OK)**: +```json +{ + "detectedTerms": [ + { + "term": "MSA", + "position": {"line": 5, "offset": 42}, + "confidence": 0.92, + "category": "기술", + "highlight": true + } + ], + "totalCount": 8, + "highlightInfo": [...] +} +``` + +#### GET /terms/{term}/explain +맥락 기반 용어 설명 + +**유저스토리**: UFR-RAG-020 +**Controller**: ExplanationController + +**Path Parameters**: +- `term` (string, required): 용어명 + +**Query Parameters**: +- `meetingId` (uuid, required): 회의 ID +- `context` (string, optional): 현재 회의 맥락 + +**Response (200 OK)**: +```json +{ + "term": "MSA", + "basicDefinition": "Microservices Architecture의 약자로, 애플리케이션을 작은 독립적인 서비스로 나누는 아키텍처 패턴", + "contextualMeaning": "이번 프로젝트에서는 확장성과 독립 배포를 위해 MSA를 적용하기로 결정", + "useCases": [ + "2024년 프로젝트 X에서 주문/결제/배송 서비스를 독립적으로 구성", + "서비스별 독립 배포로 배포 시간 70% 단축" + ], + "relatedProjects": [...], + "pastDiscussions": [...], + "references": [...] +} +``` + +--- + +### 6. 실시간 추천 (Suggestion) + +#### POST /suggestions/discussion +논의사항 제안 + +**유저스토리**: UFR-AI-010 +**Controller**: SuggestionController + +**Request Body**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "transcriptText": "프로젝트 일정에 대해 논의했습니다..." +} +``` + +**Response (200 OK)**: +```json +{ + "suggestions": [ + { + "id": "880e8400-e29b-41d4-a716-446655440003", + "topic": "보안 요구사항 검토", + "reason": "안건에 포함되어 있으나 아직 논의되지 않음", + "priority": "HIGH", + "relatedAgenda": "프로젝트 개요", + "estimatedTime": 15 + } + ], + "totalCount": 3, + "timestamp": "2025-01-23T10:35:00Z" +} +``` + +#### POST /suggestions/decision +결정사항 제안 + +**유저스토리**: UFR-AI-010 +**Controller**: SuggestionController + +**Request Body**: +```json +{ + "meetingId": "550e8400-e29b-41d4-a716-446655440000", + "transcriptText": "React로 프론트엔드를 개발하기로 했습니다..." +} +``` + +**Response (200 OK)**: +```json +{ + "suggestions": [ + { + "id": "990e8400-e29b-41d4-a716-446655440004", + "content": "React로 프론트엔드 개발", + "category": "기술", + "decisionMaker": "김철수", + "participants": ["김철수", "이영희"], + "confidence": 0.85, + "extractedFrom": "프론트엔드는 React로 개발하기로 했습니다", + "context": "팀원 대부분이 React 경험이 있어 개발 속도가 빠를 것으로 예상" + } + ], + "totalCount": 4, + "timestamp": "2025-01-23T10:35:00Z" +} +``` + +--- + +## 공통 응답 + +### 에러 응답 + +#### 400 Bad Request +```json +{ + "error": "BAD_REQUEST", + "message": "필수 파라미터가 누락되었습니다", + "timestamp": "2025-01-23T10:30:00Z" +} +``` + +#### 404 Not Found +```json +{ + "error": "NOT_FOUND", + "message": "해당 회의록을 찾을 수 없습니다", + "timestamp": "2025-01-23T10:30:00Z" +} +``` + +#### 500 Internal Server Error +```json +{ + "error": "INTERNAL_SERVER_ERROR", + "message": "서버 처리 중 오류가 발생했습니다", + "timestamp": "2025-01-23T10:30:00Z" +} +``` + +--- + +## 인증 (Security) + +모든 API는 JWT 토큰 기반 인증을 사용합니다. + +**인증 방식**: Bearer Token + +**요청 헤더**: +``` +Authorization: Bearer +``` + +--- + +## API 확인 방법 + +### Swagger Editor 사용 + +1. https://editor.swagger.io/ 접근 +2. 생성된 `ai-service-api.yaml` 파일 내용 복사 +3. Swagger Editor에 붙여넣기 +4. 우측 패널에서 API 문서 확인 +5. "Try it out" 기능으로 테스트 가능 + +### Swagger UI 로컬 실행 + +```bash +# Swagger UI Docker 실행 +docker run -p 8080:8080 \ + -e SWAGGER_JSON=/api/ai-service-api.yaml \ + -v C:\Users\KTDS\home\workspace\HGZero\design\backend\api:/api \ + swaggerapi/swagger-ui + +# 브라우저에서 접근 +# http://localhost:8080 +``` + +--- + +## 설계 원칙 준수 사항 + +### ✅ API 설계 가이드 준수 +- OpenAPI 3.0.3 스펙 사용 +- 모든 엔드포인트에 x-user-story, x-controller 명시 +- 완전한 Request/Response 스키마 정의 +- Example 데이터 포함 + +### ✅ 유저스토리 매칭 +- UFR-AI-010: 회의록 자동 작성 (실시간 추천 포함) +- UFR-AI-020: Todo 자동 추출 +- UFR-AI-030: 회의록 개선 +- UFR-AI-040: 관련 회의록 연결 +- UFR-RAG-010: 전문용어 감지 +- UFR-RAG-020: 맥락 기반 용어 설명 + +### ✅ 내부 시퀀스 일치 +- 모든 API는 내부 시퀀스 설계와 일관성 유지 +- LLM 프롬프트 파라미터 포함 +- RAG 검색 결과 구조 반영 + +### ✅ 서버 URL 설정 +- SwaggerHub Mock Server (첫 번째) +- Local/Dev/Production 서버 URL 포함 + +--- + +## 차별화 포인트 반영 + +### 1. 맥락 기반 용어 설명 +- 단순 정의가 아닌 조직 내 실제 사용 맥락 제공 +- 과거 회의록, 사내 문서, 업무 이력 통합 검색 +- 실용적인 설명으로 업무 지식 부족 해소 + +### 2. 강화된 Todo 연결 +- AI 자동 추출 + 담당자 자동 식별 +- 회의록과 양방향 연결 +- Meeting Service로 실시간 전달 + +### 3. 프롬프팅 기반 회의록 개선 +- 7가지 프롬프트 유형 지원 +- 원본 보존 + 버전 관리 +- 사용자 정의 프롬프트 지원 + +### 4. 실시간 추천 +- 논의사항 제안 (빠진 안건 자동 감지) +- 결정사항 제안 (패턴 기반 자동 추출) +- 회의 진행 중 실시간 제공 + +--- + +## 처리 시간 예상 + +| API | 평균 처리 시간 | +|-----|---------------| +| 회의록 자동 작성 | 8-13초 | +| Todo 자동 추출 | 4-7초 | +| 회의록 개선 | 5-9초 | +| 관련 회의록 연결 | 5-8초 | +| 전문용어 감지 | 3-5초 | +| 맥락 기반 용어 설명 | 5-8초 | +| 논의사항 제안 | 2.5-3.5초 | +| 결정사항 제안 | 2.5-3.5초 | + +--- + +## 문서 이력 + +| 버전 | 작성일 | 작성자 | 변경 내용 | +|------|--------|--------|----------| +| 1.0 | 2025-01-23 | 준호 (Backend Developer) | AI Service API 설계 완료 | diff --git a/design/backend/api/stt-service-api.yaml b/design/backend/api/stt-service-api.yaml new file mode 100644 index 0000000..0c95dda --- /dev/null +++ b/design/backend/api/stt-service-api.yaml @@ -0,0 +1,1306 @@ +openapi: 3.0.3 +info: + title: STT Service API + description: | + 회의록 작성 및 공유 개선 서비스 - STT Service API 명세 + + **핵심 기능:** + - 음성 녹음 시작/중지 관리 + - 실시간 음성-텍스트 변환 (스트리밍) + - 배치 음성-텍스트 변환 + - 화자 식별 및 관리 + - Azure Speech Service 통합 + + **차별화 포인트:** + - 기본 기능 (Hygiene Factor) - 경쟁사 대부분 제공 + - 실시간 스트리밍 처리로 즉각적인 자막 제공 + - 화자 자동 식별 (90% 이상 정확도) + version: 1.0.0 + contact: + name: STT Service Team + email: stt-team@example.com + +servers: + - url: https://api.example.com/stt/v1 + description: Production Server + - url: https://dev-api.example.com/stt/v1 + description: Development Server + - url: http://localhost:8083/api/v1 + description: Local Development Server + +tags: + - name: Recording + description: 음성 녹음 관리 API + - name: Transcription + description: 음성-텍스트 변환 API + - name: Speaker + description: 화자 식별 및 관리 API + +paths: + /recordings/prepare: + post: + tags: + - Recording + summary: 회의 녹음 준비 + description: | + 회의 시작 시 녹음 세션을 준비하고 Azure Speech Service 초기화 + + **처리 흐름:** + 1. 녹음 세션 검증 (중복 방지) + 2. DB에 녹음 정보 생성 + 3. Azure Speech 인식기 초기화 + 4. Blob Storage 저장 경로 생성 + 5. RecordingStarted 이벤트 발행 + operationId: prepareRecording + x-user-story: UFR-STT-010 + x-controller: RecordingController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrepareRecordingRequest' + examples: + normal: + summary: 일반 회의 녹음 준비 + value: + meetingId: "MTG-2025-001" + sessionId: "SESSION-12345" + language: "ko-KR" + attendeeCount: 5 + responses: + '200': + description: 녹음 준비 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/PrepareRecordingResponse' + examples: + success: + summary: 준비 성공 + value: + recordingId: "REC-20250123-001" + sessionId: "SESSION-12345" + status: "READY" + streamUrl: "wss://api.example.com/stt/v1/ws/stt/SESSION-12345" + storagePath: "recordings/MTG-2025-001/SESSION-12345.wav" + estimatedInitTime: 1100 + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: 녹음 세션 중복 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "RECORDING_ALREADY_EXISTS" + message: "이미 진행 중인 녹음 세션이 있습니다" + timestamp: "2025-01-23T10:30:00Z" + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /recordings/{recordingId}/start: + post: + tags: + - Recording + summary: 음성 녹음 시작 + description: | + 준비된 녹음 세션의 실제 녹음을 시작 + + **처리 흐름:** + 1. 녹음 상태를 'RECORDING'으로 업데이트 + 2. 시작 시간 기록 + 3. WebSocket 연결 활성화 + operationId: startRecording + x-user-story: UFR-STT-010 + x-controller: RecordingController + parameters: + - $ref: '#/components/parameters/RecordingIdParam' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StartRecordingRequest' + example: + startedBy: "USER-123" + recordingMode: "REAL_TIME" + responses: + '200': + description: 녹음 시작 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RecordingStatusResponse' + example: + recordingId: "REC-20250123-001" + status: "RECORDING" + startTime: "2025-01-23T10:30:00Z" + duration: 0 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /recordings/{recordingId}/stop: + post: + tags: + - Recording + summary: 음성 녹음 중지 + description: | + 진행 중인 녹음을 중지하고 최종 파일 저장 + + **처리 흐름:** + 1. 녹음 상태를 'STOPPED'으로 업데이트 + 2. 종료 시간 및 총 시간 기록 + 3. Azure Blob에 최종 파일 저장 + 4. RecordingStopped 이벤트 발행 + operationId: stopRecording + x-user-story: UFR-STT-010 + x-controller: RecordingController + parameters: + - $ref: '#/components/parameters/RecordingIdParam' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StopRecordingRequest' + example: + stoppedBy: "USER-123" + reason: "MEETING_END" + responses: + '200': + description: 녹음 중지 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RecordingStatusResponse' + example: + recordingId: "REC-20250123-001" + status: "STOPPED" + startTime: "2025-01-23T10:30:00Z" + endTime: "2025-01-23T11:00:00Z" + duration: 1800 + fileSize: 172800000 + storagePath: "recordings/MTG-2025-001/SESSION-12345.wav" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /recordings/{recordingId}: + get: + tags: + - Recording + summary: 녹음 정보 조회 + description: 특정 녹음 세션의 상세 정보 조회 + operationId: getRecording + x-user-story: UFR-STT-010 + x-controller: RecordingController + parameters: + - $ref: '#/components/parameters/RecordingIdParam' + responses: + '200': + description: 녹음 정보 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RecordingDetailResponse' + example: + recordingId: "REC-20250123-001" + meetingId: "MTG-2025-001" + sessionId: "SESSION-12345" + status: "RECORDING" + startTime: "2025-01-23T10:30:00Z" + duration: 300 + speakerCount: 3 + segmentCount: 45 + storagePath: "recordings/MTG-2025-001/SESSION-12345.wav" + language: "ko-KR" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /transcripts/stream: + post: + tags: + - Transcription + summary: 실시간 음성-텍스트 변환 (스트리밍) + description: | + WebSocket을 통한 실시간 음성 스트림 변환 + + **처리 흐름:** + 1. 음성 데이터 스트림 수신 + 2. Azure Speech Service 실시간 인식 + 3. 화자 식별 + 4. 신뢰도 검증 (70% threshold) + 5. DB에 세그먼트 저장 + 6. TranscriptSegmentReady 이벤트 발행 + 7. WebSocket으로 실시간 자막 전송 + + **성능:** + - 실시간 인식 지연: < 1초 + - 처리 시간: 1-4초 + operationId: streamTranscription + x-user-story: UFR-STT-020 + x-controller: TranscriptController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StreamTranscriptionRequest' + example: + recordingId: "REC-20250123-001" + audioData: "base64_encoded_audio_chunk" + timestamp: 1234567890 + chunkIndex: 42 + responses: + '200': + description: 변환 성공 (부분 결과) + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptionSegmentResponse' + example: + transcriptId: "TRS-SEG-001" + recordingId: "REC-20250123-001" + text: "안녕하세요, 오늘 회의를 시작하겠습니다." + speakerId: "SPK-001" + speakerName: "김철수" + timestamp: 1234567890 + duration: 3.5 + confidence: 0.92 + warningFlag: false + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /transcripts/batch: + post: + tags: + - Transcription + summary: 배치 음성-텍스트 변환 + description: | + 전체 오디오 파일을 배치로 변환 (비동기 처리) + + **처리 흐름:** + 1. 전체 오디오 파일 업로드 + 2. Azure Batch Transcription Job 생성 + 3. 비동기 처리 시작 + 4. Job ID 반환 (202 Accepted) + 5. 처리 완료 시 Callback으로 결과 수신 + + **처리 시간:** + - 파일 업로드: 1-2초 + - Azure 배치 처리: 5-30초 (파일 크기 따라) + - 총 처리 시간: 7-33초 + operationId: batchTranscription + x-user-story: UFR-STT-020 + x-controller: TranscriptController + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/BatchTranscriptionRequest' + responses: + '202': + description: 배치 작업 접수됨 + content: + application/json: + schema: + $ref: '#/components/schemas/BatchTranscriptionResponse' + example: + jobId: "JOB-20250123-001" + recordingId: "REC-20250123-001" + status: "PROCESSING" + estimatedCompletionTime: "2025-01-23T10:31:00Z" + callbackUrl: "https://api.example.com/stt/v1/transcripts/callback" + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /transcripts/callback: + post: + tags: + - Transcription + summary: 배치 변환 완료 콜백 + description: | + Azure Speech Service로부터 배치 변환 완료 콜백 수신 + + **처리 흐름:** + 1. 배치 결과 수신 + 2. 세그먼트별 DB 저장 + 3. 전체 텍스트 병합 + 4. TranscriptionCompleted 이벤트 발행 + operationId: batchTranscriptionCallback + x-user-story: UFR-STT-020 + x-controller: TranscriptController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCallbackRequest' + responses: + '200': + description: 콜백 처리 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptionCompleteResponse' + example: + jobId: "JOB-20250123-001" + recordingId: "REC-20250123-001" + status: "COMPLETED" + segmentCount: 120 + totalDuration: 1800 + averageConfidence: 0.88 + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + + /transcripts/{recordingId}: + get: + tags: + - Transcription + summary: 변환 텍스트 전체 조회 + description: | + 특정 녹음의 전체 변환 텍스트 조회 + + **응답 데이터:** + - 전체 텍스트 + - 화자별 세그먼트 목록 + - 타임스탬프 정보 + - 신뢰도 점수 + operationId: getTranscription + x-user-story: UFR-STT-020 + x-controller: TranscriptController + parameters: + - $ref: '#/components/parameters/RecordingIdParam' + - name: includeSegments + in: query + description: 세그먼트 상세 정보 포함 여부 + required: false + schema: + type: boolean + default: false + - name: speakerId + in: query + description: 특정 화자의 발언만 필터링 + required: false + schema: + type: string + example: "SPK-001" + responses: + '200': + description: 변환 텍스트 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TranscriptionResponse' + example: + recordingId: "REC-20250123-001" + fullText: "김철수: 안녕하세요...\n이영희: 네, 안녕하세요..." + segmentCount: 120 + totalDuration: 1800 + averageConfidence: 0.88 + speakerCount: 3 + segments: + - transcriptId: "TRS-SEG-001" + text: "안녕하세요, 오늘 회의를 시작하겠습니다." + speakerId: "SPK-001" + speakerName: "김철수" + timestamp: 0 + duration: 3.5 + confidence: 0.92 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /speakers/identify: + post: + tags: + - Speaker + summary: 화자 식별 + description: | + 음성 데이터로부터 화자 식별 + + **처리 흐름:** + 1. Voice signature 생성 + 2. 기존 프로필과 매칭 + 3. 신규 화자 자동 등록 + 4. 화자 정보 반환 + + **정확도:** + - 화자 식별 정확도: > 90% + - 처리 시간: ~300ms + operationId: identifySpeaker + x-user-story: UFR-STT-010 + x-controller: SpeakerController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/IdentifySpeakerRequest' + example: + recordingId: "REC-20250123-001" + audioFrame: "base64_encoded_audio_frame" + timestamp: 1234567890 + responses: + '200': + description: 화자 식별 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/SpeakerIdentificationResponse' + example: + speakerId: "SPK-001" + speakerName: "김철수" + confidence: 0.95 + isNewSpeaker: false + profileId: "PROFILE-12345" + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /speakers/{speakerId}: + get: + tags: + - Speaker + summary: 화자 정보 조회 + description: 특정 화자의 상세 정보 조회 + operationId: getSpeaker + x-user-story: UFR-STT-010 + x-controller: SpeakerController + parameters: + - name: speakerId + in: path + description: 화자 ID + required: true + schema: + type: string + example: "SPK-001" + responses: + '200': + description: 화자 정보 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/SpeakerDetailResponse' + example: + speakerId: "SPK-001" + speakerName: "김철수" + profileId: "PROFILE-12345" + totalSegments: 45 + totalDuration: 450 + averageConfidence: 0.92 + firstAppeared: "2025-01-23T10:30:15Z" + lastAppeared: "2025-01-23T11:00:00Z" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + put: + tags: + - Speaker + summary: 화자 정보 업데이트 + description: 화자 이름 등 정보 수정 + operationId: updateSpeaker + x-user-story: UFR-STT-010 + x-controller: SpeakerController + parameters: + - name: speakerId + in: path + description: 화자 ID + required: true + schema: + type: string + example: "SPK-001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateSpeakerRequest' + example: + speakerName: "김철수 팀장" + userId: "USER-123" + responses: + '200': + description: 화자 정보 업데이트 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/SpeakerDetailResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + + /recordings/{recordingId}/speakers: + get: + tags: + - Speaker + summary: 녹음의 화자 목록 조회 + description: 특정 녹음에 참여한 모든 화자 목록 조회 + operationId: getRecordingSpeakers + x-user-story: UFR-STT-010 + x-controller: SpeakerController + parameters: + - $ref: '#/components/parameters/RecordingIdParam' + responses: + '200': + description: 화자 목록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/SpeakerListResponse' + example: + recordingId: "REC-20250123-001" + speakerCount: 3 + speakers: + - speakerId: "SPK-001" + speakerName: "김철수" + segmentCount: 45 + totalDuration: 450 + speakingRatio: 0.45 + - speakerId: "SPK-002" + speakerName: "이영희" + segmentCount: 38 + totalDuration: 380 + speakingRatio: 0.38 + - speakerId: "SPK-003" + speakerName: "박민수" + segmentCount: 17 + totalDuration: 170 + speakingRatio: 0.17 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증 + + parameters: + RecordingIdParam: + name: recordingId + in: path + description: 녹음 ID + required: true + schema: + type: string + example: "REC-20250123-001" + + schemas: + PrepareRecordingRequest: + type: object + required: + - meetingId + - sessionId + properties: + meetingId: + type: string + description: 회의 ID + example: "MTG-2025-001" + sessionId: + type: string + description: 세션 ID + example: "SESSION-12345" + language: + type: string + description: 음성 인식 언어 + default: "ko-KR" + enum: + - ko-KR + - en-US + - ja-JP + example: "ko-KR" + attendeeCount: + type: integer + description: 참석자 수 (화자 식별 최적화용) + minimum: 1 + maximum: 50 + example: 5 + + PrepareRecordingResponse: + type: object + properties: + recordingId: + type: string + description: 생성된 녹음 ID + example: "REC-20250123-001" + sessionId: + type: string + description: 세션 ID + example: "SESSION-12345" + status: + type: string + description: 녹음 상태 + enum: + - READY + - RECORDING + - STOPPED + - ERROR + example: "READY" + streamUrl: + type: string + description: WebSocket 스트리밍 URL + example: "wss://api.example.com/stt/v1/ws/stt/SESSION-12345" + storagePath: + type: string + description: Azure Blob Storage 저장 경로 + example: "recordings/MTG-2025-001/SESSION-12345.wav" + estimatedInitTime: + type: integer + description: 예상 초기화 시간 (ms) + example: 1100 + + StartRecordingRequest: + type: object + required: + - startedBy + properties: + startedBy: + type: string + description: 녹음 시작자 사용자 ID + example: "USER-123" + recordingMode: + type: string + description: 녹음 모드 + enum: + - REAL_TIME + - BATCH + default: "REAL_TIME" + example: "REAL_TIME" + + StopRecordingRequest: + type: object + required: + - stoppedBy + properties: + stoppedBy: + type: string + description: 녹음 중지자 사용자 ID + example: "USER-123" + reason: + type: string + description: 중지 사유 + enum: + - MEETING_END + - USER_REQUEST + - ERROR + - TIMEOUT + example: "MEETING_END" + + RecordingStatusResponse: + type: object + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + status: + type: string + description: 녹음 상태 + enum: + - READY + - RECORDING + - STOPPED + - ERROR + example: "RECORDING" + startTime: + type: string + format: date-time + description: 녹음 시작 시간 + example: "2025-01-23T10:30:00Z" + endTime: + type: string + format: date-time + description: 녹음 종료 시간 + example: "2025-01-23T11:00:00Z" + duration: + type: integer + description: 녹음 시간 (초) + example: 1800 + fileSize: + type: integer + description: 파일 크기 (bytes) + example: 172800000 + storagePath: + type: string + description: 저장 경로 + example: "recordings/MTG-2025-001/SESSION-12345.wav" + + RecordingDetailResponse: + type: object + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + meetingId: + type: string + description: 회의 ID + example: "MTG-2025-001" + sessionId: + type: string + description: 세션 ID + example: "SESSION-12345" + status: + type: string + description: 녹음 상태 + enum: + - READY + - RECORDING + - STOPPED + - ERROR + example: "RECORDING" + startTime: + type: string + format: date-time + description: 시작 시간 + example: "2025-01-23T10:30:00Z" + endTime: + type: string + format: date-time + description: 종료 시간 + example: "2025-01-23T11:00:00Z" + duration: + type: integer + description: 녹음 시간 (초) + example: 300 + speakerCount: + type: integer + description: 화자 수 + example: 3 + segmentCount: + type: integer + description: 세그먼트 수 + example: 45 + storagePath: + type: string + description: 저장 경로 + example: "recordings/MTG-2025-001/SESSION-12345.wav" + language: + type: string + description: 음성 인식 언어 + example: "ko-KR" + + StreamTranscriptionRequest: + type: object + required: + - recordingId + - audioData + - timestamp + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + audioData: + type: string + format: byte + description: Base64 인코딩된 오디오 청크 + example: "UklGRiQAAABXQVZFZm10IBAAAAABA..." + timestamp: + type: integer + description: 타임스탬프 (ms) + example: 1234567890 + chunkIndex: + type: integer + description: 청크 순서 번호 + example: 42 + + TranscriptionSegmentResponse: + type: object + properties: + transcriptId: + type: string + description: 변환 텍스트 세그먼트 ID + example: "TRS-SEG-001" + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + text: + type: string + description: 변환된 텍스트 + example: "안녕하세요, 오늘 회의를 시작하겠습니다." + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + speakerName: + type: string + description: 화자 이름 + example: "김철수" + timestamp: + type: integer + description: 타임스탬프 (ms) + example: 1234567890 + duration: + type: number + format: float + description: 발언 시간 (초) + example: 3.5 + confidence: + type: number + format: float + description: 신뢰도 점수 (0-1) + minimum: 0 + maximum: 1 + example: 0.92 + warningFlag: + type: boolean + description: 낮은 신뢰도 경고 플래그 (< 60%) + example: false + + BatchTranscriptionRequest: + type: object + required: + - recordingId + - audioFile + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + audioFile: + type: string + format: binary + description: 오디오 파일 (WAV, MP3 등) + language: + type: string + description: 음성 인식 언어 + default: "ko-KR" + example: "ko-KR" + callbackUrl: + type: string + format: uri + description: 처리 완료 콜백 URL + example: "https://api.example.com/stt/v1/transcripts/callback" + + BatchTranscriptionResponse: + type: object + properties: + jobId: + type: string + description: 배치 작업 ID + example: "JOB-20250123-001" + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + status: + type: string + description: 작업 상태 + enum: + - QUEUED + - PROCESSING + - COMPLETED + - FAILED + example: "PROCESSING" + estimatedCompletionTime: + type: string + format: date-time + description: 예상 완료 시간 + example: "2025-01-23T10:31:00Z" + callbackUrl: + type: string + format: uri + description: 콜백 URL + example: "https://api.example.com/stt/v1/transcripts/callback" + + BatchCallbackRequest: + type: object + required: + - jobId + - status + - segments + properties: + jobId: + type: string + description: 배치 작업 ID + example: "JOB-20250123-001" + status: + type: string + description: 작업 상태 + enum: + - COMPLETED + - FAILED + example: "COMPLETED" + segments: + type: array + description: 변환 세그먼트 목록 + items: + $ref: '#/components/schemas/TranscriptionSegment' + error: + type: string + description: 오류 메시지 (실패 시) + example: "Audio file format not supported" + + TranscriptionSegment: + type: object + properties: + text: + type: string + description: 변환된 텍스트 + example: "안녕하세요, 오늘 회의를 시작하겠습니다." + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + timestamp: + type: integer + description: 시작 타임스탬프 (ms) + example: 1234567890 + duration: + type: number + format: float + description: 발언 시간 (초) + example: 3.5 + confidence: + type: number + format: float + description: 신뢰도 점수 (0-1) + example: 0.92 + + TranscriptionCompleteResponse: + type: object + properties: + jobId: + type: string + description: 배치 작업 ID + example: "JOB-20250123-001" + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + status: + type: string + description: 작업 상태 + enum: + - COMPLETED + - FAILED + example: "COMPLETED" + segmentCount: + type: integer + description: 총 세그먼트 수 + example: 120 + totalDuration: + type: integer + description: 총 시간 (초) + example: 1800 + averageConfidence: + type: number + format: float + description: 평균 신뢰도 점수 + example: 0.88 + + TranscriptionResponse: + type: object + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + fullText: + type: string + description: 전체 변환 텍스트 + example: "김철수: 안녕하세요...\n이영희: 네, 안녕하세요..." + segmentCount: + type: integer + description: 총 세그먼트 수 + example: 120 + totalDuration: + type: integer + description: 총 시간 (초) + example: 1800 + averageConfidence: + type: number + format: float + description: 평균 신뢰도 점수 + example: 0.88 + speakerCount: + type: integer + description: 화자 수 + example: 3 + segments: + type: array + description: 세그먼트 목록 + items: + $ref: '#/components/schemas/TranscriptionSegmentDetail' + + TranscriptionSegmentDetail: + type: object + properties: + transcriptId: + type: string + description: 세그먼트 ID + example: "TRS-SEG-001" + text: + type: string + description: 변환된 텍스트 + example: "안녕하세요, 오늘 회의를 시작하겠습니다." + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + speakerName: + type: string + description: 화자 이름 + example: "김철수" + timestamp: + type: integer + description: 타임스탬프 (ms) + example: 0 + duration: + type: number + format: float + description: 발언 시간 (초) + example: 3.5 + confidence: + type: number + format: float + description: 신뢰도 점수 + example: 0.92 + + IdentifySpeakerRequest: + type: object + required: + - recordingId + - audioFrame + - timestamp + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + audioFrame: + type: string + format: byte + description: Base64 인코딩된 오디오 프레임 + example: "UklGRiQAAABXQVZFZm10IBAAAAABA..." + timestamp: + type: integer + description: 타임스탬프 (ms) + example: 1234567890 + + SpeakerIdentificationResponse: + type: object + properties: + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + speakerName: + type: string + description: 화자 이름 + example: "김철수" + confidence: + type: number + format: float + description: 식별 신뢰도 (0-1) + minimum: 0 + maximum: 1 + example: 0.95 + isNewSpeaker: + type: boolean + description: 신규 화자 여부 + example: false + profileId: + type: string + description: Azure Speaker Profile ID + example: "PROFILE-12345" + + SpeakerDetailResponse: + type: object + properties: + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + speakerName: + type: string + description: 화자 이름 + example: "김철수" + profileId: + type: string + description: Azure Speaker Profile ID + example: "PROFILE-12345" + userId: + type: string + description: 연결된 사용자 ID + example: "USER-123" + totalSegments: + type: integer + description: 총 발언 세그먼트 수 + example: 45 + totalDuration: + type: integer + description: 총 발언 시간 (초) + example: 450 + averageConfidence: + type: number + format: float + description: 평균 식별 신뢰도 + example: 0.92 + firstAppeared: + type: string + format: date-time + description: 최초 등장 시간 + example: "2025-01-23T10:30:15Z" + lastAppeared: + type: string + format: date-time + description: 최근 등장 시간 + example: "2025-01-23T11:00:00Z" + + UpdateSpeakerRequest: + type: object + properties: + speakerName: + type: string + description: 화자 이름 + example: "김철수 팀장" + userId: + type: string + description: 연결할 사용자 ID + example: "USER-123" + + SpeakerListResponse: + type: object + properties: + recordingId: + type: string + description: 녹음 ID + example: "REC-20250123-001" + speakerCount: + type: integer + description: 화자 수 + example: 3 + speakers: + type: array + description: 화자 목록 + items: + $ref: '#/components/schemas/SpeakerSummary' + + SpeakerSummary: + type: object + properties: + speakerId: + type: string + description: 화자 ID + example: "SPK-001" + speakerName: + type: string + description: 화자 이름 + example: "김철수" + segmentCount: + type: integer + description: 발언 세그먼트 수 + example: 45 + totalDuration: + type: integer + description: 총 발언 시간 (초) + example: 450 + speakingRatio: + type: number + format: float + description: 발언 비율 (0-1) + example: 0.45 + + ErrorResponse: + type: object + properties: + code: + type: string + description: 오류 코드 + example: "RECORDING_NOT_FOUND" + message: + type: string + description: 오류 메시지 + example: "녹음을 찾을 수 없습니다" + timestamp: + type: string + format: date-time + description: 오류 발생 시간 + example: "2025-01-23T10:30:00Z" + path: + type: string + description: 요청 경로 + example: "/api/v1/recordings/REC-999" + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INVALID_REQUEST" + message: "요청 파라미터가 올바르지 않습니다" + timestamp: "2025-01-23T10:30:00Z" + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "RECORDING_NOT_FOUND" + message: "녹음을 찾을 수 없습니다" + timestamp: "2025-01-23T10:30:00Z" + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INTERNAL_SERVER_ERROR" + message: "서버 오류가 발생했습니다" + timestamp: "2025-01-23T10:30:00Z" diff --git a/design/backend/api/user-service-api.yaml b/design/backend/api/user-service-api.yaml new file mode 100644 index 0000000..a44a374 --- /dev/null +++ b/design/backend/api/user-service-api.yaml @@ -0,0 +1,531 @@ +openapi: 3.0.3 +info: + title: User Service API + description: | + 회의록 작성 및 공유 개선 서비스의 사용자 인증 전용 서비스 + + **핵심 기능:** + - LDAP 기반 사용자 인증 + - JWT 토큰 발급 및 검증 + - 세션 관리 (Access Token + Refresh Token) + + **보안:** + - LDAP 인증 (LDAPS, port 636) + - JWT Bearer 토큰 + - 계정 잠금 (5회 실패 시 30분) + - Refresh Token (Redis 저장, 7일 TTL) + + version: 1.0.0 + contact: + name: Backend Team + email: backend@company.com + +servers: + - url: https://api.meeting.company.com + description: Production Server + - url: https://dev-api.meeting.company.com + description: Development Server + - url: http://localhost:8081 + description: Local Development + +tags: + - name: Authentication + description: 사용자 인증 관리 API + +paths: + /api/v1/auth/login: + post: + summary: 사용자 로그인 + description: | + LDAP 인증을 통한 사용자 로그인 처리 + + **처리 흐름:** + 1. LDAP 서버에 사용자 인증 요청 + 2. 인증 성공 시 사용자 정보 조회 + 3. 신규 사용자일 경우 자동 등록 + 4. JWT Access Token 및 Refresh Token 발급 + 5. Refresh Token을 Redis에 저장 (7일 TTL) + 6. 최종 로그인 일시 업데이트 + + **인증 실패 처리:** + - 5회 실패 시 계정 30분 잠금 + - 잠금 상태에서는 로그인 불가 + operationId: login + x-user-story: AFR-USER-010 + x-controller: UserController + tags: + - Authentication + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + examples: + normal: + summary: 정상 로그인 요청 + value: + username: "user001" + password: "P@ssw0rd123!" + responses: + '200': + description: 로그인 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + examples: + success: + summary: 로그인 성공 응답 + value: + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + tokenType: "Bearer" + expiresIn: 3600 + user: + userId: "user001" + username: "user001" + name: "홍길동" + email: "hong@company.com" + department: "개발팀" + title: "선임" + roles: + - "USER" + '401': + $ref: '#/components/responses/UnauthorizedError' + '403': + description: 계정 잠금 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + accountLocked: + summary: 계정 잠금 응답 + value: + error: + code: "ACCOUNT_LOCKED" + message: "계정이 잠겼습니다" + details: "비밀번호 5회 실패로 30분간 계정이 잠겼습니다. 잠금 해제 시간: 2025-10-23T13:30:00Z" + timestamp: "2025-10-23T13:00:00Z" + path: "/api/v1/auth/login" + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/auth/refresh: + post: + summary: Access Token 갱신 + description: | + Refresh Token을 사용하여 새로운 Access Token 발급 + + **처리 흐름:** + 1. Refresh Token 검증 + 2. Redis에서 Refresh Token 존재 확인 + 3. 새로운 Access Token 발급 + 4. Refresh Token 갱신 (옵션) + operationId: refreshToken + x-user-story: AFR-USER-010 + x-controller: UserController + tags: + - Authentication + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshTokenRequest' + examples: + normal: + summary: 토큰 갱신 요청 + value: + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + responses: + '200': + description: 토큰 갱신 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshTokenResponse' + examples: + success: + summary: 토큰 갱신 성공 응답 + value: + accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + refreshToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + tokenType: "Bearer" + expiresIn: 3600 + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/auth/logout: + post: + summary: 로그아웃 + description: | + 사용자 로그아웃 처리 + + **처리 흐름:** + 1. Access Token에서 사용자 정보 추출 + 2. Redis에서 Refresh Token 삭제 + 3. 로그아웃 완료 + operationId: logout + x-user-story: AFR-USER-010 + x-controller: UserController + tags: + - Authentication + security: + - BearerAuth: [] + responses: + '200': + description: 로그아웃 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LogoutResponse' + examples: + success: + summary: 로그아웃 성공 응답 + value: + message: "로그아웃되었습니다" + timestamp: "2025-10-23T14:30:00Z" + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/auth/validate: + get: + summary: 토큰 검증 + description: | + JWT Access Token 유효성 검증 + + **검증 항목:** + - 토큰 서명 검증 + - 토큰 만료 시간 확인 + - 사용자 정보 조회 (옵션) + + **용도:** + - API Gateway에서 인증 검증 + - 다른 서비스에서 사용자 정보 조회 + operationId: validateToken + x-user-story: AFR-USER-010 + x-controller: UserController + tags: + - Authentication + security: + - BearerAuth: [] + parameters: + - name: includeUserInfo + in: query + description: 사용자 정보 포함 여부 + required: false + schema: + type: boolean + default: false + responses: + '200': + description: 토큰 유효 + content: + application/json: + schema: + $ref: '#/components/schemas/ValidateTokenResponse' + examples: + valid: + summary: 토큰 유효 응답 + value: + valid: true + userId: "user001" + username: "user001" + roles: + - "USER" + expiresAt: "2025-10-23T15:00:00Z" + validWithUserInfo: + summary: 사용자 정보 포함 응답 + value: + valid: true + userId: "user001" + username: "user001" + roles: + - "USER" + expiresAt: "2025-10-23T15:00:00Z" + user: + userId: "user001" + username: "user001" + name: "홍길동" + email: "hong@company.com" + department: "개발팀" + title: "선임" + '401': + $ref: '#/components/responses/UnauthorizedError' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT Bearer Token + + schemas: + LoginRequest: + type: object + required: + - username + - password + properties: + username: + type: string + description: 사용자명 (사번) + minLength: 3 + maxLength: 50 + example: "user001" + password: + type: string + description: 비밀번호 + format: password + minLength: 8 + maxLength: 100 + example: "P@ssw0rd123!" + + LoginResponse: + type: object + required: + - accessToken + - refreshToken + - tokenType + - expiresIn + - user + properties: + accessToken: + type: string + description: JWT Access Token + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMDAxIiwidXNlcm5hbWUiOiJ1c2VyMDAxIiwicm9sZXMiOlsiVVNFUiJdLCJleHAiOjE3MzAwMDAwMDB9.signature" + refreshToken: + type: string + description: Refresh Token (7일 유효) + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMDAxIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3MzA2MDQ4MDB9.signature" + tokenType: + type: string + description: 토큰 타입 + enum: + - Bearer + example: "Bearer" + expiresIn: + type: integer + description: Access Token 만료 시간 (초) + example: 3600 + user: + $ref: '#/components/schemas/UserInfo' + + RefreshTokenRequest: + type: object + required: + - refreshToken + properties: + refreshToken: + type: string + description: Refresh Token + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + + RefreshTokenResponse: + type: object + required: + - accessToken + - refreshToken + - tokenType + - expiresIn + properties: + accessToken: + type: string + description: 새로운 JWT Access Token + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + refreshToken: + type: string + description: 새로운 Refresh Token (옵션) + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + tokenType: + type: string + description: 토큰 타입 + enum: + - Bearer + example: "Bearer" + expiresIn: + type: integer + description: Access Token 만료 시간 (초) + example: 3600 + + LogoutResponse: + type: object + required: + - message + - timestamp + properties: + message: + type: string + description: 로그아웃 완료 메시지 + example: "로그아웃되었습니다" + timestamp: + type: string + format: date-time + description: 로그아웃 시간 + example: "2025-10-23T14:30:00Z" + + ValidateTokenResponse: + type: object + required: + - valid + - userId + - username + - roles + - expiresAt + properties: + valid: + type: boolean + description: 토큰 유효 여부 + example: true + userId: + type: string + description: 사용자 ID + example: "user001" + username: + type: string + description: 사용자명 + example: "user001" + roles: + type: array + description: 사용자 권한 목록 + items: + type: string + example: + - "USER" + expiresAt: + type: string + format: date-time + description: 토큰 만료 시간 + example: "2025-10-23T15:00:00Z" + user: + $ref: '#/components/schemas/UserInfo' + description: 사용자 정보 (includeUserInfo=true 시) + + UserInfo: + type: object + required: + - userId + - username + - name + - email + properties: + userId: + type: string + description: 사용자 ID + example: "user001" + username: + type: string + description: 사용자명 (사번) + example: "user001" + name: + type: string + description: 이름 + example: "홍길동" + email: + type: string + format: email + description: 이메일 + example: "hong@company.com" + department: + type: string + description: 부서 + example: "개발팀" + title: + type: string + description: 직급 + example: "선임" + roles: + type: array + description: 권한 목록 + items: + type: string + example: + - "USER" + + ErrorResponse: + type: object + required: + - error + properties: + error: + type: object + required: + - code + - message + - timestamp + - path + properties: + code: + type: string + description: 에러 코드 + example: "AUTHENTICATION_FAILED" + message: + type: string + description: 에러 메시지 + example: "인증에 실패했습니다" + details: + type: string + description: 상세 에러 정보 + example: "사용자명 또는 비밀번호가 올바르지 않습니다" + timestamp: + type: string + format: date-time + description: 에러 발생 시간 + example: "2025-10-23T12:00:00Z" + path: + type: string + description: 요청 경로 + example: "/api/v1/auth/login" + + responses: + UnauthorizedError: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authenticationFailed: + summary: 인증 실패 + value: + error: + code: "AUTHENTICATION_FAILED" + message: "인증에 실패했습니다" + details: "사용자명 또는 비밀번호가 올바르지 않습니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/v1/auth/login" + invalidToken: + summary: 유효하지 않은 토큰 + value: + error: + code: "INVALID_TOKEN" + message: "유효하지 않은 토큰입니다" + details: "토큰이 만료되었거나 형식이 올바르지 않습니다" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/v1/auth/validate" + + InternalServerError: + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: 서버 오류 + value: + error: + code: "INTERNAL_SERVER_ERROR" + message: "서버 오류가 발생했습니다" + details: "일시적인 오류입니다. 잠시 후 다시 시도해주세요" + timestamp: "2025-10-23T12:00:00Z" + path: "/api/v1/auth/login"