From 3405d233ee53d7242eeef06bc32f7277089dcc96 Mon Sep 17 00:00:00 2001 From: djeon Date: Wed, 22 Oct 2025 14:25:29 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=8B=9C=ED=80=80?= =?UTF-8?q?=EC=8A=A4=20=EC=84=A4=EA=B3=84=20=ED=8C=8C=EC=9D=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=9C=A0=EC=A0=80=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 외부 시퀀스 설계 파일 삭제 - 유저스토리 수정 - 샘플 외부 시퀀스 파일 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- claude/sample-external-sequence.puml | 126 +++++++ .../outer/01-사용자인증및대시보드조회.puml | 164 --------- .../sequence/outer/02-회의예약및초대.puml | 186 ---------- .../outer/03-회의시작및템플릿선택.puml | 73 ---- .../outer/04-회의진행및실시간회의록작성.puml | 108 ------ .../outer/05-회의종료및Todo자동추출.puml | 140 -------- .../sequence/outer/06-회의록확정및공유.puml | 183 ---------- .../outer/07-회의록목록조회및상세조회.puml | 62 ---- .../outer/08-맥락기반용어설명제공.puml | 78 ----- .../outer/09-Todo완료및회의록반영.puml | 94 ----- design/backend/sequence/outer/README.md | 328 ------------------ .../sequence/outer/Todo완료및회의록반영.puml | 211 ----------- .../sequence/outer/회의록조회및수정.png | Bin 41927 -> 0 bytes .../sequence/outer/회의록조회및수정.puml | 123 ------- .../sequence/outer/회의록확정및공유.puml | 243 ------------- .../sequence/outer/회의시작및회의록작성.puml | 170 --------- .../sequence/outer/회의예약및초대.puml | 91 ----- .../sequence/outer/회의종료및Todo추출.puml | 194 ----------- design/userstory.md | 150 +++++++- 19 files changed, 263 insertions(+), 2461 deletions(-) create mode 100644 claude/sample-external-sequence.puml delete mode 100644 design/backend/sequence/outer/01-사용자인증및대시보드조회.puml delete mode 100644 design/backend/sequence/outer/02-회의예약및초대.puml delete mode 100644 design/backend/sequence/outer/03-회의시작및템플릿선택.puml delete mode 100644 design/backend/sequence/outer/04-회의진행및실시간회의록작성.puml delete mode 100644 design/backend/sequence/outer/05-회의종료및Todo자동추출.puml delete mode 100644 design/backend/sequence/outer/06-회의록확정및공유.puml delete mode 100644 design/backend/sequence/outer/07-회의록목록조회및상세조회.puml delete mode 100644 design/backend/sequence/outer/08-맥락기반용어설명제공.puml delete mode 100644 design/backend/sequence/outer/09-Todo완료및회의록반영.puml delete mode 100644 design/backend/sequence/outer/README.md delete mode 100644 design/backend/sequence/outer/Todo완료및회의록반영.puml delete mode 100644 design/backend/sequence/outer/회의록조회및수정.png delete mode 100644 design/backend/sequence/outer/회의록조회및수정.puml delete mode 100644 design/backend/sequence/outer/회의록확정및공유.puml delete mode 100644 design/backend/sequence/outer/회의시작및회의록작성.puml delete mode 100644 design/backend/sequence/outer/회의예약및초대.puml delete mode 100644 design/backend/sequence/outer/회의종료및Todo추출.puml diff --git a/claude/sample-external-sequence.puml b/claude/sample-external-sequence.puml new file mode 100644 index 0000000..08614b3 --- /dev/null +++ b/claude/sample-external-sequence.puml @@ -0,0 +1,126 @@ +@startuml 로그인플로우 +!theme mono + +title 로그인 플로우 - 외부 시퀀스 다이어그램 + +actor "Mobile Client" as Client +participant "API Gateway" as Gateway +participant "User Service" as UserService +database "Redis Cache" as Redis + +== 로그인 화면 접근 == +Client -> Gateway: GET /login-page +activate Gateway +Gateway -> Client: 200 OK (로그인 화면) +deactivate Gateway + +== 로그인 처리 == +Client -> Gateway: POST /api/v1/users/auth/login\n{userId, password} +activate Gateway + +Gateway -> UserService: 로그인 요청 전달 +activate UserService + +UserService -> UserService: 사용자 인증 처리\n(비밀번호 검증) + +alt 인증 성공 + UserService -> UserService: JWT 토큰 생성 + UserService -> Redis: 세션 정보 저장\n(userId, token, TTL) + activate Redis + Redis -> UserService: 저장 완료 + deactivate Redis + + UserService -> Gateway: 200 OK\n{token, userId, profile} + Gateway -> Client: 로그인 성공\n{token, profile} + + Client -> Client: 토큰 저장\n(localStorage) + Client -> Client: 대시보드로 이동 + +else 인증 실패 + UserService -> Gateway: 401 Unauthorized\n{error: "아이디 또는 비밀번호를 확인해주세요"} + Gateway -> Client: 로그인 실패 메시지 + + alt 5회 연속 실패 + UserService -> Redis: 계정 잠금 정보 저장\n(userId, lockTime: 30분) + UserService -> Gateway: 423 Locked\n{error: "30분간 계정 잠금"} + Gateway -> Client: 계정 잠금 안내 + end +end + +deactivate UserService +deactivate Gateway + +== 로그인 상태 확인 == +Client -> Gateway: GET /api/v1/users/profile\n(Authorization: Bearer {token}) +activate Gateway + +Gateway -> Gateway: JWT 토큰 검증 + +alt 토큰 유효 + Gateway -> Redis: 세션 조회\n(userId) + activate Redis + Redis -> Gateway: 세션 정보 반환 + deactivate Redis + + Gateway -> UserService: 프로필 조회 + activate UserService + UserService -> Gateway: 사용자 프로필 반환 + deactivate UserService + + Gateway -> Client: 200 OK\n{userId, name, email, avatar} + Client -> Client: 프로필 표시\n(헤더 아바타) +else 토큰 무효 + Gateway -> Client: 401 Unauthorized + Client -> Client: 로그인 화면으로 이동 +end + +deactivate Gateway + +== 로그아웃 처리 == +Client -> Client: 로그아웃 확인 다이얼로그 표시 +Client -> Gateway: POST /api/v1/users/auth/logout\n(Authorization: Bearer {token}) +activate Gateway + +Gateway -> UserService: 로그아웃 요청 +activate UserService + +UserService -> Redis: 세션 삭제\n(userId) +activate Redis +Redis -> UserService: 삭제 완료 +deactivate Redis + +UserService -> Gateway: 200 OK\n{message: "안전하게 로그아웃되었습니다"} +deactivate UserService + +Gateway -> Client: 로그아웃 완료 +deactivate Gateway + +Client -> Client: 토큰 삭제\n(localStorage) +Client -> Client: 로그인 화면으로 이동 + +note right of Client +대시보드(01) 화면의 헤더에서 +프로필 아바타를 클릭하면 +드롭다운 메뉴가 표시됨: +- 내 정보 보기 +- 프로필 편집 +- 로그아웃 +end note + +note right of UserService +로그인 시 검증 사항: +- 아이디/비밀번호 확인 +- 계정 잠금 상태 확인 +- 연속 실패 횟수 체크 +- JWT 토큰 생성 및 발급 +end note + +note right of Redis +캐시 저장 정보: +- 세션 정보 (토큰, 사용자ID) +- 로그인 실패 횟수 +- 계정 잠금 상태 +- TTL 기반 자동 만료 +end note + +@enduml \ No newline at end of file diff --git a/design/backend/sequence/outer/01-사용자인증및대시보드조회.puml b/design/backend/sequence/outer/01-사용자인증및대시보드조회.puml deleted file mode 100644 index ec0102a..0000000 --- a/design/backend/sequence/outer/01-사용자인증및대시보드조회.puml +++ /dev/null @@ -1,164 +0,0 @@ -@startuml 01-사용자인증및대시보드조회 -!theme mono - -title 사용자 인증 및 대시보드 조회 (User Authentication and Dashboard) - -actor "사용자" as User -participant "Web App" as WebApp -participant "API Gateway" as Gateway -participant "User Service" as UserService -participant "Meeting Service" as MeetingService -participant "Todo Service" as TodoService -participant "Redis\n(Cache)" as Redis -database "User DB\n(PostgreSQL)" as UserDB -database "Meeting DB\n(PostgreSQL)" as MeetingDB -database "Todo DB\n(PostgreSQL)" as TodoDB - -== 1. 사용자 인증 (User Authentication) == - -User -> WebApp: 1.1. 로그인 시도\n(사번, 비밀번호 입력) -activate WebApp - -WebApp -> Gateway: 1.2. POST /api/auth/login\n{employeeId, password} -activate Gateway - -Gateway -> UserService: 1.3. 인증 요청\nPOST /auth/login -activate UserService - -UserService -> UserService: 1.4. LDAP 연동 인증\n(사번, 비밀번호 검증) -note right - LDAP 서버와 통신하여 - 사용자 인증 처리 -end note - -alt 인증 성공 - UserService -> UserDB: 1.5. 사용자 정보 조회\nSELECT * FROM users WHERE employee_id = ? - activate UserDB - UserDB --> UserService: 사용자 정보 반환 - deactivate UserDB - - UserService -> UserService: 1.6. JWT 토큰 생성\n(userId, 권한 정보 포함) - - UserService -> Redis: 1.7. 사용자 프로필 캐싱\nSET user:profile:{userId}\n(TTL: 30분) - activate Redis - Redis --> UserService: 캐싱 완료 - deactivate Redis - - UserService --> Gateway: 1.8. 인증 성공 응답\n{token, userId, userName} - deactivate UserService - - Gateway --> WebApp: 1.9. 로그인 성공\n{token, userId, userName} - deactivate Gateway - - WebApp -> WebApp: 1.10. JWT 토큰 저장\n(LocalStorage) - - WebApp --> User: 1.11. 로그인 성공 메시지 - -else 인증 실패 - UserService --> Gateway: 인증 실패 (401 Unauthorized) - Gateway --> WebApp: 인증 실패 - WebApp --> User: 로그인 실패 메시지 -end - -== 2. 대시보드 데이터 조회 (Dashboard Data Loading) == - -WebApp -> Gateway: 2.1. GET /api/dashboard\n(Authorization: Bearer {token}) -activate Gateway - -Gateway -> Gateway: 2.2. JWT 토큰 검증 -note right - 토큰 유효성 검증 - 사용자 권한 확인 -end note - -Gateway -> MeetingService: 2.3. 예정된 회의 조회\nGET /meetings/upcoming?userId={userId} -activate MeetingService - -MeetingService -> Redis: 2.4. 캐시 확인\nGET meeting:upcoming:{userId} -activate Redis -Redis --> MeetingService: Cache Miss (캐시 없음) -deactivate Redis - -MeetingService -> MeetingDB: 2.5. 예정된 회의 조회\nSELECT * FROM meetings\nWHERE user_id = ? AND status = 'SCHEDULED'\nORDER BY meeting_date LIMIT 5 -activate MeetingDB -MeetingDB --> MeetingService: 예정된 회의 목록 (최대 5개) -deactivate MeetingDB - -MeetingService -> Redis: 2.6. 조회 결과 캐싱\nSET meeting:upcoming:{userId}\n(TTL: 10분) -activate Redis -Redis --> MeetingService: 캐싱 완료 -deactivate Redis - -MeetingService --> Gateway: 2.7. 예정된 회의 목록 반환 -deactivate MeetingService - -||| - -Gateway -> TodoService: 2.8. 진행 중 Todo 조회\nGET /todos/in-progress?userId={userId} -activate TodoService - -TodoService -> Redis: 2.9. 캐시 확인\nGET todo:user:{userId} -activate Redis -Redis --> TodoService: Cache Hit (캐시 존재) -note right - Cache-Aside 패턴: - 캐시에서 직접 반환하여 - DB 부하 감소 -end note -deactivate Redis - -TodoService --> Gateway: 2.10. 진행 중 Todo 목록 반환\n(캐시에서 조회) -deactivate TodoService - -||| - -Gateway -> MeetingService: 2.11. 최근 회의록 조회\nGET /transcripts/recent?userId={userId} -activate MeetingService - -MeetingService -> Redis: 2.12. 캐시 확인\nGET transcript:recent:{userId} -activate Redis -Redis --> MeetingService: Cache Miss -deactivate Redis - -MeetingService -> MeetingDB: 2.13. 최근 회의록 조회\nSELECT * FROM transcripts\nWHERE user_id = ? OR shared_with LIKE '%{userId}%'\nORDER BY created_at DESC LIMIT 6 -activate MeetingDB -MeetingDB --> MeetingService: 최근 회의록 목록\n(내 회의록 3개 + 공유받은 회의록 3개) -deactivate MeetingDB - -MeetingService -> Redis: 2.14. 조회 결과 캐싱\nSET transcript:recent:{userId}\n(TTL: 10분) -activate Redis -Redis --> MeetingService: 캐싱 완료 -deactivate Redis - -MeetingService --> Gateway: 2.15. 최근 회의록 목록 반환 -deactivate MeetingService - -||| - -Gateway -> TodoService: 2.16. Todo 통계 조회\nGET /todos/stats?userId={userId} -activate TodoService - -TodoService -> Redis: 2.17. 캐시 확인\nGET todo:stats:{userId} -activate Redis -Redis --> TodoService: Cache Hit -deactivate Redis - -TodoService --> Gateway: 2.18. Todo 통계 반환\n{total, inProgress, completionRate} -deactivate TodoService - -||| - -Gateway --> WebApp: 2.19. 대시보드 데이터 통합 응답\n{\n upcomingMeetings: [...],\n inProgressTodos: [...],\n recentTranscripts: [...],\n todoStats: {...}\n} -deactivate Gateway - -WebApp -> WebApp: 2.20. 대시보드 UI 렌더링\n- 통계 카드 표시\n- 예정된 회의 목록\n- 진행 중 Todo 목록\n- 최근 회의록 목록 -note right - 반응형 레이아웃: - - 데스크톱: 좌측 사이드바 - - 모바일: 하단 탭 바 -end note - -WebApp --> User: 2.21. 대시보드 화면 표시\n(맞춤형 정보 제공) -deactivate WebApp - -@enduml diff --git a/design/backend/sequence/outer/02-회의예약및초대.puml b/design/backend/sequence/outer/02-회의예약및초대.puml deleted file mode 100644 index 5a866f0..0000000 --- a/design/backend/sequence/outer/02-회의예약및초대.puml +++ /dev/null @@ -1,186 +0,0 @@ -@startuml 02-회의예약및초대 -!theme mono - -title 회의 예약 및 초대 (Meeting Reservation and Invitation) - -actor "사용자\n(회의록 작성자)" as User -participant "Web App" as WebApp -participant "API Gateway" as Gateway -participant "Meeting Service" as MeetingService -participant "User Service" as UserService -participant "Notification Service" as NotificationService -participant "RabbitMQ\n(Message Broker)" as RabbitMQ -participant "Redis\n(Cache)" as Redis -database "Meeting DB\n(PostgreSQL)" as MeetingDB -database "User DB\n(PostgreSQL)" as UserDB -database "Notification DB\n(PostgreSQL)" as NotificationDB - -== 1. 회의 예약 (Meeting Reservation) == - -User -> WebApp: 1.1. 회의 예약 화면 접근\n(플로팅 액션 버튼 → 📅 회의 예약) -activate WebApp - -WebApp --> User: 1.2. 회의 예약 폼 표시\n(제목, 날짜/시간, 장소, 참석자) -note right - 입력 필드: - - 회의 제목 (최대 100자, 필수) - - 날짜/시간 (필수) - - 장소 (최대 200자, 선택) - - 참석자 목록 (이메일, 최소 1명 필수) -end note - -User -> WebApp: 1.3. 회의 정보 입력 완료\n- 제목: "프로젝트 킥오프 회의"\n- 날짜: 2025-02-01 14:00\n- 장소: "회의실 A"\n- 참석자: ["user1@company.com", "user2@company.com"] - -WebApp -> WebApp: 1.4. 입력 유효성 검증\n- 필수 필드 확인\n- 날짜/시간 형식 검증\n- 이메일 형식 검증 - -WebApp -> Gateway: 1.5. POST /api/meetings\n{\n title: "프로젝트 킥오프 회의",\n dateTime: "2025-02-01T14:00:00",\n location: "회의실 A",\n participants: ["user1@company.com", "user2@company.com"]\n}\n(Authorization: Bearer {token}) -activate Gateway - -Gateway -> Gateway: 1.6. JWT 토큰 검증\n- 사용자 인증 확인\n- 회의 생성 권한 확인 - -Gateway -> MeetingService: 1.7. 회의 생성 요청\nPOST /meetings -activate MeetingService - -MeetingService -> MeetingService: 1.8. 회의 데이터 생성\n- 회의 ID 생성 (UUID)\n- 생성 시간 기록\n- 상태: SCHEDULED - -MeetingService -> MeetingDB: 1.9. 회의 정보 저장\nINSERT INTO meetings\n(id, title, date_time, location, creator_id, status, created_at)\nVALUES (?, ?, ?, ?, ?, 'SCHEDULED', NOW()) -activate MeetingDB -MeetingDB --> MeetingService: 회의 생성 완료 -deactivate MeetingDB - -MeetingService -> MeetingDB: 1.10. 참석자 정보 저장\nINSERT INTO meeting_participants\n(meeting_id, user_email, role)\nVALUES (?, ?, 'PARTICIPANT') -activate MeetingDB -MeetingDB --> MeetingService: 참석자 저장 완료 -deactivate MeetingDB - -MeetingService -> Redis: 1.11. 회의 정보 캐싱\nSET meeting:info:{meetingId}\n{\n title, dateTime, location,\n participants, status\n}\n(TTL: 10분) -activate Redis -Redis --> MeetingService: 캐싱 완료 -note right - Cache-Aside 패턴: - 생성 즉시 캐싱하여 - 후속 조회 성능 향상 -end note -deactivate Redis - -== 2. 이벤트 발행 (Event Publishing) == - -MeetingService -> RabbitMQ: 1.12. MeetingCreated 이벤트 발행\n- Exchange: meeting.events\n- Routing Key: meeting.created\n- Payload: {\n meetingId,\n title,\n dateTime,\n location,\n creatorId,\n participants: [user1@company.com, user2@company.com]\n } -activate RabbitMQ -note right - Publisher-Subscriber 패턴: - 이벤트 기반 느슨한 결합 -end note - -RabbitMQ --> MeetingService: 1.13. 이벤트 발행 성공 -deactivate RabbitMQ - -MeetingService --> Gateway: 1.14. 회의 생성 응답\n{\n meetingId,\n title,\n dateTime,\n location,\n status: "SCHEDULED",\n createdAt\n} -deactivate MeetingService - -Gateway --> WebApp: 1.15. 회의 예약 성공 응답 -deactivate Gateway - -WebApp --> User: 1.16. 예약 완료 메시지 표시\n"회의가 성공적으로 예약되었습니다." -deactivate WebApp - -== 3. 초대 알림 발송 (Invitation Notification) == - -RabbitMQ -> NotificationService: 2.1. MeetingCreated 이벤트 수신\n- Queue: notification.meeting.queue -activate NotificationService -activate RabbitMQ - -NotificationService -> NotificationService: 2.2. 이벤트 처리\n- 참석자 목록 추출\n- 알림 템플릿 로딩 - -NotificationService -> UserService: 2.3. 참석자 정보 조회\nGET /users/by-emails?emails=user1@company.com,user2@company.com -activate UserService - -UserService -> Redis: 2.4. 캐시 확인\nMGET user:profile:user1, user:profile:user2 -activate Redis -Redis --> UserService: Cache Hit (일부 캐시 존재) -deactivate Redis - -UserService -> UserDB: 2.5. 캐시 미스 사용자 DB 조회\nSELECT * FROM users\nWHERE email IN (?, ?) -activate UserDB -UserDB --> UserService: 사용자 정보 반환 -deactivate UserDB - -UserService -> Redis: 2.6. 조회 결과 캐싱\nSET user:profile:{userId}\n(TTL: 30분) -activate Redis -Redis --> UserService: 캐싱 완료 -deactivate Redis - -UserService --> NotificationService: 2.7. 참석자 정보 반환\n[{userId, name, email}, ...] -deactivate UserService - -NotificationService -> NotificationService: 2.8. 알림 메시지 생성\n- 제목: "[회의 초대] 프로젝트 킥오프 회의"\n- 내용: "홍길동님께서 회의에 초대하셨습니다.\n 일시: 2025-02-01 14:00\n 장소: 회의실 A" - -NotificationService -> NotificationDB: 2.9. 알림 기록 저장\nINSERT INTO notifications\n(meeting_id, recipient_email, type, content, status, created_at)\nVALUES (?, ?, 'MEETING_INVITATION', ?, 'PENDING', NOW()) -activate NotificationDB -NotificationDB --> NotificationService: 알림 기록 저장 완료 -deactivate NotificationDB - -loop 각 참석자에 대해 - NotificationService -> NotificationService: 2.10. 이메일 발송\n- To: user1@company.com\n- Subject: [회의 초대] 프로젝트 킥오프 회의\n- Body: 회의 상세 정보 + 참여 링크 - note right - Email/SMS Service 연동: - - SMTP 또는 SendGrid 사용 - - 발송 실패 시 재시도 (최대 3회) - end note - - NotificationService -> NotificationDB: 2.11. 알림 상태 업데이트\nUPDATE notifications\nSET status = 'SENT', sent_at = NOW()\nWHERE id = ? - activate NotificationDB - NotificationDB --> NotificationService: 업데이트 완료 - deactivate NotificationDB -end - -NotificationService --> RabbitMQ: 2.12. 이벤트 처리 완료 (ACK) -deactivate RabbitMQ - -NotificationService -> NotificationService: 2.13. 리마인더 일정 생성\n- 회의 시작 30분 전 알림\n- 일정: 2025-02-01 13:30 -note right - Queue-Based Load Leveling: - 리마인더는 별도 큐로 관리 - (reminder.queue) -end note - -deactivate NotificationService - -== 4. 결과 확인 (Result Confirmation) == - -User -> WebApp: 3.1. 대시보드 화면 새로고침 -activate WebApp - -WebApp -> Gateway: 3.2. GET /api/dashboard\n(Authorization: Bearer {token}) -activate Gateway - -Gateway -> MeetingService: 3.3. 예정된 회의 조회\nGET /meetings/upcoming?userId={userId} -activate MeetingService - -MeetingService -> Redis: 3.4. 캐시 확인\nGET meeting:upcoming:{userId} -activate Redis -Redis --> MeetingService: Cache Miss (새로운 회의 추가로 캐시 무효화됨) -deactivate Redis - -MeetingService -> MeetingDB: 3.5. 예정된 회의 조회\nSELECT * FROM meetings\nWHERE user_id = ? AND status = 'SCHEDULED'\nORDER BY meeting_date LIMIT 5 -activate MeetingDB -MeetingDB --> MeetingService: 예정된 회의 목록\n(새로 생성한 회의 포함) -deactivate MeetingDB - -MeetingService -> Redis: 3.6. 조회 결과 재캐싱\nSET meeting:upcoming:{userId}\n(TTL: 10분) -activate Redis -Redis --> MeetingService: 캐싱 완료 -deactivate Redis - -MeetingService --> Gateway: 3.7. 예정된 회의 목록 반환 -deactivate MeetingService - -Gateway --> WebApp: 3.8. 대시보드 데이터 반환 -deactivate Gateway - -WebApp -> WebApp: 3.9. 대시보드 UI 업데이트\n- 예정된 회의 개수 증가\n- 새 회의 목록에 표시 - -WebApp --> User: 3.10. 업데이트된 대시보드 표시\n(새로 예약한 회의 확인 가능) -deactivate WebApp - -@enduml diff --git a/design/backend/sequence/outer/03-회의시작및템플릿선택.puml b/design/backend/sequence/outer/03-회의시작및템플릿선택.puml deleted file mode 100644 index d3f592b..0000000 --- a/design/backend/sequence/outer/03-회의시작및템플릿선택.puml +++ /dev/null @@ -1,73 +0,0 @@ -@startuml 회의시작및템플릿선택 -!theme mono - -title 회의 시작 및 템플릿 선택 (Flow 3) - -actor "사용자" as User -participant "Web App" as Web -participant "API Gateway" as Gateway -participant "Meeting Service" as Meeting -participant "STT Service" as STT -participant "RabbitMQ" as MQ -participant "Redis" as Cache -database "PostgreSQL" as DB - -== 템플릿 선택 == -User -> Web: 회의 템플릿 선택\n(일반/스크럼/킥오프/주간) -Web -> Gateway: GET /api/meetings/templates -Gateway -> Meeting: 템플릿 목록 조회 -Meeting -> Cache: 템플릿 캐시 확인 -alt 캐시 존재 - Cache --> Meeting: 템플릿 목록 반환 -else 캐시 미존재 - Meeting -> DB: SELECT * FROM templates - DB --> Meeting: 템플릿 데이터 - Meeting -> Cache: 템플릿 캐시 저장 -end -Meeting --> Gateway: 템플릿 목록 응답 -Gateway --> Web: 200 OK + 템플릿 목록 -Web --> User: 템플릿 선택 UI 표시 - -== 회의 시작 == -User -> Web: 회의 시작 버튼 클릭 -Web -> Gateway: POST /api/meetings/{meetingId}/start\n+ templateId -Gateway -> Meeting: 회의 시작 요청 - -Meeting -> DB: BEGIN TRANSACTION -Meeting -> DB: UPDATE meetings\nSET status='ongoing',\nstarted_at=NOW() -DB --> Meeting: 업데이트 완료 - -Meeting -> DB: INSERT INTO meeting_sessions\n(meeting_id, template_id,\nsession_status='active') -DB --> Meeting: 세션 생성 완료 - -Meeting -> DB: INSERT INTO meeting_transcripts\n(session_id, template_id,\ncontent='{}') -DB --> Meeting: 회의록 초기화 완료 - -Meeting -> DB: COMMIT -Meeting -> Cache: 회의 상태 캐시 업데이트\n(status='ongoing') - -== 이벤트 발행 == -Meeting ->> MQ: Publish "MeetingStarted" Event\n{\n meetingId,\n sessionId,\n templateId,\n startedAt,\n participants\n} -note right - 비동기 이벤트 발행 - - Exchange: meeting.events - - Routing Key: meeting.started -end note - -Meeting --> Gateway: 200 OK + sessionId -Gateway --> Web: 회의 시작 성공 응답 -Web --> User: 회의 진행 화면 전환 - -== STT 서비스 시작 == -MQ -->> STT: Consume "MeetingStarted" Event -STT -> STT: 오디오 녹음 세션 초기화 -STT -> Cache: 녹음 세션 상태 저장\n(sessionId, status='recording') -STT -> STT: 마이크 활성화 및\n오디오 스트림 시작 - -note over STT - STT 서비스 준비 완료 - - 5초 단위 음성 인식 시작 - - 실시간 텍스트 변환 대기 -end note - -@enduml diff --git a/design/backend/sequence/outer/04-회의진행및실시간회의록작성.puml b/design/backend/sequence/outer/04-회의진행및실시간회의록작성.puml deleted file mode 100644 index 13bcde3..0000000 --- a/design/backend/sequence/outer/04-회의진행및실시간회의록작성.puml +++ /dev/null @@ -1,108 +0,0 @@ -@startuml 회의진행및실시간회의록작성 -!theme mono - -title 회의 진행 및 실시간 회의록 작성 (Flow 4) - -actor "참여자 A" as UserA -actor "참여자 B" as UserB -participant "Web App A\n(WebSocket)" as WebA -participant "Web App B\n(WebSocket)" as WebB -participant "STT Service" as STT -participant "AI Service" as AI -participant "Collaboration\nService" as Collab -participant "RabbitMQ" as MQ -participant "Redis" as Cache -database "PostgreSQL" as DB - -== 실시간 음성 인식 (5초 단위) == -loop 5초마다 반복 - STT -> STT: 오디오 청크 수집\n(5초분) - STT -> STT: 음성-텍스트 변환\n(Speech-to-Text) - - STT ->> MQ: Publish "TranscriptReady" Event\n{\n sessionId,\n timestamp,\n speaker: "참여자A",\n text: "변환된 텍스트",\n confidence: 0.95\n} - note right - 비동기 이벤트 발행 - - Exchange: transcript.events - - Routing Key: transcript.ready - end note -end - -== AI 기반 회의록 자동 생성 == -MQ -->> AI: Consume "TranscriptReady" Event - -AI -> AI: 텍스트 분석 및 구조화\n(키워드, 주제, 맥락 파악) - -AI -> Cache: 템플릿 구조 조회\n(sessionId) -Cache --> AI: 템플릿 섹션 정보\n(agenda, decisions, tasks) - -AI -> AI: 템플릿 섹션에 맞춰\n내용 자동 분류 -note right - 예시: - - "결정사항" → decisions - - "TODO" → action_items - - "논의내용" → discussions -end note - -AI -> DB: INSERT INTO transcript_segments\n(session_id, section,\ncontent, timestamp) -DB --> AI: 세그먼트 저장 완료 - -AI ->> MQ: Publish "TranscriptUpdated" Event\n{\n sessionId,\n segmentId,\n section,\n content,\n updatedAt\n} - -== 실시간 협업 및 동기화 == -MQ -->> Collab: Consume "TranscriptUpdated" Event - -Collab -> Cache: 현재 회의록 상태 조회 -Cache --> Collab: 최신 회의록 데이터 - -Collab -> Collab: 변경 사항 감지 및\n충돌 검사 (CRDT 알고리즘) - -Collab ->> WebA: WebSocket Push\n{\n type: "transcript.update",\n section: "decisions",\n content: "...",\n author: "AI"\n} -Collab ->> WebB: WebSocket Push\n{\n type: "transcript.update",\n section: "decisions",\n content: "...",\n author: "AI"\n} - -WebA --> UserA: 회의록 UI 실시간 업데이트 -WebB --> UserB: 회의록 UI 실시간 업데이트 - -== 참여자 실시간 편집 == -UserA -> WebA: 회의록 내용 수정\n("결정사항" 섹션 편집) -WebA ->> Collab: WebSocket Send\n{\n type: "edit",\n section: "decisions",\n content: "수정된 내용",\n cursorPosition: 42\n} - -Collab -> Collab: 동시 편집 충돌 검사\n(Operational Transform) - -alt 충돌 없음 - Collab -> DB: UPDATE transcript_segments\nSET content='수정된 내용' - DB --> Collab: 업데이트 완료 - - Collab -> Cache: 최신 회의록 캐시 갱신 - - Collab ->> WebB: WebSocket Push\n{\n type: "peer.edit",\n section: "decisions",\n content: "수정된 내용",\n author: "참여자A"\n} - WebB --> UserB: 참여자A의 수정사항 반영 - -else 충돌 발생 - Collab -> Collab: 충돌 해결 알고리즘 적용\n(Last-Write-Wins + Merge) - - Collab -> DB: UPDATE transcript_segments\nSET content='병합된 내용' - - Collab ->> WebA: WebSocket Push\n{\n type: "conflict.resolved",\n mergedContent: "병합된 내용"\n} - Collab ->> WebB: WebSocket Push\n{\n type: "conflict.resolved",\n mergedContent: "병합된 내용"\n} - - WebA --> UserA: 병합된 내용으로 UI 업데이트 - WebB --> UserB: 병합된 내용으로 UI 업데이트 -end - -== 실시간 커서 및 선택 영역 공유 == -UserB -> WebB: 텍스트 선택 또는 커서 이동 -WebB ->> Collab: WebSocket Send\n{\n type: "cursor.move",\n userId: "userB",\n position: 120,\n selection: {start: 100, end: 130}\n} - -Collab ->> WebA: WebSocket Push\n{\n type: "peer.cursor",\n userId: "userB",\n userName: "참여자B",\n position: 120,\n color: "#FF5733"\n} - -WebA --> UserA: 참여자B의 커서 위치 표시\n(다른 색상으로 강조) - -note over WebA, WebB - 실시간 협업 기능: - - 동시 편집 지원 (CRDT) - - 충돌 자동 해결 - - 커서 위치 공유 - - 변경 이력 추적 -end note - -@enduml diff --git a/design/backend/sequence/outer/05-회의종료및Todo자동추출.puml b/design/backend/sequence/outer/05-회의종료및Todo자동추출.puml deleted file mode 100644 index 05690bb..0000000 --- a/design/backend/sequence/outer/05-회의종료및Todo자동추출.puml +++ /dev/null @@ -1,140 +0,0 @@ -@startuml -!theme mono - -title Flow 5: 회의 종료 및 Todo 자동 추출 (Meeting End and Auto Todo Extraction) - -actor "사용자\n(User)" as User -participant "Web App" as Web -participant "API Gateway" as Gateway -participant "Meeting Service" as Meeting -participant "STT Service" as STT -participant "AI Service" as AI -participant "Todo Service" as Todo -participant "Notification Service" as Notification -queue "RabbitMQ" as MQ -database "Redis" as Cache -database "PostgreSQL" as DB - -== 회의 종료 (Meeting End) == - -User -> Web: 1. 회의 종료 버튼 클릭\n(Click "End Meeting" button) -activate Web - -Web -> Gateway: 2. PATCH /api/meetings/{meetingId}/end\n(End meeting request) -activate Gateway - -Gateway -> Meeting: 3. 회의 종료 요청\n(Forward end meeting request) -activate Meeting - -Meeting -> DB: 4. 회의 상태 조회\n(Query meeting status) -activate DB -DB --> Meeting: 회의 정보 반환\n(Return meeting info) -deactivate DB - -Meeting -> DB: 5. 회의 상태를 "ended"로 업데이트\n종료 시간 기록\n(Update status to "ended", record end time) -activate DB -DB --> Meeting: 업데이트 완료\n(Update confirmed) -deactivate DB - -Meeting -> DB: 6. 회의 통계 생성\n(참여자 수, 발언 시간, 총 시간 등)\n(Generate meeting statistics) -activate DB -DB --> Meeting: 통계 저장 완료\n(Statistics saved) -deactivate DB - -Meeting -> Cache: 7. 캐시 업데이트\n(Update cache with final status) -activate Cache -Cache --> Meeting: 캐시 업데이트 완료\n(Cache updated) -deactivate Cache - -Meeting ->> MQ: 8. "MeetingEnded" 이벤트 발행\n(Publish "MeetingEnded" event)\n{meetingId, endTime, stats} -activate MQ - -Meeting --> Gateway: 9. 회의 종료 응답\n(Return end meeting response) -deactivate Meeting - -Gateway --> Web: 10. 종료 확인 응답\n(Return confirmation) -deactivate Gateway - -Web --> User: 11. 회의 종료 완료 메시지 표시\n(Display meeting ended message) -deactivate Web - -== STT 녹음 중지 (Stop STT Recording) == - -MQ ->> STT: 12. "MeetingEnded" 이벤트 수신\n(Receive "MeetingEnded" event) -activate STT - -STT -> STT: 13. 녹음 중지 및 최종 처리\n(Stop recording and final processing) - -STT -> DB: 14. 최종 음성 데이터 저장\n(Save final audio data) -activate DB -DB --> STT: 저장 완료\n(Save confirmed) -deactivate DB - -STT -> DB: 15. 전사 완료 상태 업데이트\n(Update transcription status to completed) -activate DB -DB --> STT: 업데이트 완료\n(Update confirmed) -deactivate DB - -deactivate STT -deactivate MQ - -== AI Todo 자동 추출 (AI Auto Todo Extraction) == - -MQ ->> AI: 16. "MeetingEnded" 이벤트 수신\n(Receive "MeetingEnded" event) -activate MQ -activate AI - -AI -> DB: 17. 전체 회의록 조회\n(Query full transcript) -activate DB -DB --> AI: 회의록 텍스트 반환\n(Return transcript text) -deactivate DB - -AI -> AI: 18. AI 분석 수행\n- 실행 항목(Action Items) 식별\n- 담당자(Assignee) 추출\n- 마감일 추론\n(AI analysis:\n- Identify action items\n- Extract assignees\n- Infer deadlines) - -AI -> DB: 19. AI 추출 결과 저장\n(Save AI extraction results) -activate DB -DB --> AI: 저장 완료\n(Save confirmed) -deactivate DB - -AI ->> MQ: 20. "TodoExtracted" 이벤트 발행\n(Publish "TodoExtracted" event)\n{meetingId, todos: [{description, assignee, dueDate, transcriptSection}]} -deactivate AI - -== Todo 생성 및 연결 (Todo Creation and Linking) == - -MQ ->> Todo: 21. "TodoExtracted" 이벤트 수신\n(Receive "TodoExtracted" event) -activate Todo - -loop 각 Todo 항목마다 (For each todo item) - Todo -> DB: 22. Todo 생성\n- 회의 연결 (meetingId)\n- 전사 섹션 연결 (transcriptSectionId)\n- 담당자 정보\n- 마감일\n(Create todo with:\n- Meeting link\n- Transcript section link\n- Assignee info\n- Due date) - activate DB - DB --> Todo: Todo 생성 완료\n(Todo created) - deactivate DB - - Todo ->> MQ: 23. "TodoCreated" 이벤트 발행\n(Publish "TodoCreated" event)\n{todoId, meetingId, assignee} -end - -deactivate Todo - -== 담당자 알림 (Assignee Notification) == - -MQ ->> Notification: 24. "TodoCreated" 이벤트 수신\n(Receive "TodoCreated" event) -activate Notification - -Notification -> DB: 25. 담당자 정보 조회\n(Query assignee information) -activate DB -DB --> Notification: 사용자 정보 및 이메일\n(Return user info and email) -deactivate DB - -Notification -> Notification: 26. Todo 할당 이메일 생성\n- Todo 내용\n- 마감일\n- 회의록 링크\n(Generate todo assignment email:\n- Todo description\n- Due date\n- Transcript link) - -Notification -> Notification: 27. 이메일 발송\n(Send email to assignee) - -Notification -> DB: 28. 알림 전송 기록 저장\n(Save notification log) -activate DB -DB --> Notification: 저장 완료\n(Save confirmed) -deactivate DB - -deactivate Notification -deactivate MQ - -@enduml diff --git a/design/backend/sequence/outer/06-회의록확정및공유.puml b/design/backend/sequence/outer/06-회의록확정및공유.puml deleted file mode 100644 index ff6ba9a..0000000 --- a/design/backend/sequence/outer/06-회의록확정및공유.puml +++ /dev/null @@ -1,183 +0,0 @@ -@startuml -!theme mono - -title Flow 6: 회의록 확정 및 공유 (Transcript Confirmation and Sharing) - -actor "사용자\n(User)" as User -participant "Web App" as Web -participant "API Gateway" as Gateway -participant "Meeting Service" as Meeting -participant "Notification Service" as Notification -queue "RabbitMQ" as MQ -database "Redis" as Cache -database "PostgreSQL" as DB - -== 회의록 검토 (Transcript Review) == - -User -> Web: 1. 회의록 최종 검토\n(Review final transcript) -activate Web - -Web -> Gateway: 2. GET /api/meetings/{meetingId}/transcript\n(Get transcript for review) -activate Gateway - -Gateway -> Meeting: 3. 회의록 조회 요청\n(Forward transcript query) -activate Meeting - -Meeting -> Cache: 4. 캐시에서 회의록 조회\n(Check cache for transcript) -activate Cache -Cache --> Meeting: 캐시 히트/미스\n(Cache hit/miss) -deactivate Cache - -alt 캐시 미스 (Cache miss) - Meeting -> DB: 5. DB에서 회의록 조회\n(Query transcript from DB) - activate DB - DB --> Meeting: 회의록 데이터 반환\n(Return transcript data) - deactivate DB - - Meeting -> Cache: 6. 캐시에 저장\n(Store in cache) - activate Cache - Cache --> Meeting: 저장 완료\n(Stored) - deactivate Cache -end - -Meeting --> Gateway: 7. 회의록 반환\n(Return transcript) -deactivate Meeting - -Gateway --> Web: 8. 회의록 데이터\n(Return transcript data) -deactivate Gateway - -Web --> User: 9. 회의록 표시\n(Display transcript) -deactivate Web - -== 필수 항목 검증 (Validate Required Sections) == - -User -> Web: 10. 회의록 확정 요청\n(Request transcript confirmation) -activate Web - -Web -> Web: 11. 필수 항목 검증\n- 회의 제목 존재 여부\n- 참석자 목록 존재 여부\n- 주요 결정사항 작성 여부\n(Validate required sections:\n- Title exists\n- Participants list exists\n- Key decisions documented) - -alt 필수 항목 누락 (Missing required sections) - Web --> User: 12. 필수 항목 누락 경고\n(Display missing sections warning) - deactivate Web -else 검증 통과 (Validation passed) - - Web -> Gateway: 13. POST /api/meetings/{meetingId}/confirm\n(Confirm transcript request) - activate Gateway - - Gateway -> Meeting: 14. 회의록 확정 요청\n(Forward confirmation request) - activate Meeting - - Meeting -> DB: 15. 회의록 확정 처리\n- 최종 버전 생성 (version++)\n- 확정 상태로 변경 (status: "confirmed")\n- 확정 일시 기록\n(Confirm transcript:\n- Create final version\n- Update status to "confirmed"\n- Record confirmation time) - activate DB - DB --> Meeting: 확정 완료\n(Confirmation saved) - deactivate DB - - Meeting -> Cache: 16. 캐시 무효화\n(Invalidate cache) - activate Cache - Cache --> Meeting: 캐시 삭제 완료\n(Cache invalidated) - deactivate Cache - - Meeting --> Gateway: 17. 확정 완료 응답\n(Return confirmation response) - deactivate Meeting - - Gateway --> Web: 18. 확정 완료\n(Confirmation successful) - deactivate Gateway - - Web --> User: 19. 확정 완료 메시지 표시\n(Display confirmation message) - deactivate Web -end - -== 공유 설정 (Configure Sharing) == - -User -> Web: 20. 공유 설정 화면 열기\n(Open sharing configuration) -activate Web - -Web --> User: 21. 공유 옵션 표시\n- 수신자 선택\n- 권한 설정 (읽기/편집)\n- 공유 범위 (전체/일부)\n(Display sharing options:\n- Select recipients\n- Set permissions\n- Choose scope) -deactivate Web - -User -> Web: 22. 공유 설정 입력\n- 수신자 목록\n- 권한 레벨\n- 메시지 (선택)\n(Input sharing settings:\n- Recipients list\n- Permission level\n- Optional message) -activate Web - -Web -> Gateway: 23. POST /api/meetings/{meetingId}/share\n{recipients, permissions, message}\n(Submit sharing request) -activate Gateway - -Gateway -> Meeting: 24. 공유 설정 요청\n(Forward sharing request) -activate Meeting - -Meeting -> DB: 25. 공유 링크 생성\n- 고유 공유 ID 생성 (UUID)\n- 공유 권한 저장\n- 유효기간 설정 (선택)\n(Generate share link:\n- Create unique share ID\n- Save permissions\n- Set expiration if configured) -activate DB -DB --> Meeting: 공유 정보 저장 완료\n(Share info saved) -deactivate DB - -Meeting -> Cache: 26. 공유 링크 캐시 저장\n(Cache share link) -activate Cache -Cache --> Meeting: 캐시 저장 완료\n(Cached) -deactivate Cache - -Meeting ->> MQ: 27. "TranscriptShared" 이벤트 발행\n(Publish "TranscriptShared" event)\n{meetingId, shareId, recipients, permissions} -activate MQ - -Meeting --> Gateway: 28. 공유 링크 반환\n(Return share link) -deactivate Meeting - -Gateway --> Web: 29. 공유 링크 및 확인\n(Return share link) -deactivate Gateway - -Web --> User: 30. 공유 완료 메시지 및 링크 표시\n(Display share link and confirmation) -deactivate Web - -== 공유 알림 발송 (Send Share Notifications) == - -MQ ->> Notification: 31. "TranscriptShared" 이벤트 수신\n(Receive "TranscriptShared" event) -activate Notification - -loop 각 수신자마다 (For each recipient) - Notification -> DB: 32. 수신자 정보 조회\n(Query recipient information) - activate DB - DB --> Notification: 사용자 정보 및 이메일\n(Return user info and email) - deactivate DB - - Notification -> Notification: 33. 공유 알림 이메일 생성\n- 회의 정보\n- 공유 링크\n- 권한 레벨\n- 발신자 메시지\n(Generate share notification email:\n- Meeting info\n- Share link\n- Permission level\n- Sender message) - - Notification -> Notification: 34. 이메일 발송\n(Send email to recipient) - - Notification -> DB: 35. 알림 전송 기록 저장\n(Save notification log) - activate DB - DB --> Notification: 저장 완료\n(Save confirmed) - deactivate DB -end - -deactivate Notification -deactivate MQ - -== 참여자 알림 (Participant Notification) == - -note over Notification -모든 회의 참여자에게도 -회의록 확정 알림 발송 -(Send confirmation notification -to all meeting participants) -end note - -Notification ->> Notification: 36. 참여자 알림 처리\n(Process participant notifications) -activate Notification - -Notification -> DB: 37. 참여자 목록 조회\n(Query participants list) -activate DB -DB --> Notification: 참여자 정보 반환\n(Return participants info) -deactivate DB - -loop 각 참여자마다 (For each participant) - Notification -> Notification: 38. 확정 알림 이메일 생성\n- 회의록 확정 알림\n- 회의록 보기 링크\n- 주요 내용 요약\n(Generate confirmation email:\n- Transcript confirmed\n- View link\n- Summary) - - Notification -> Notification: 39. 이메일 발송\n(Send email) - - Notification -> DB: 40. 알림 기록 저장\n(Save notification log) - activate DB - DB --> Notification: 저장 완료\n(Saved) - deactivate DB -end - -deactivate Notification - -@enduml diff --git a/design/backend/sequence/outer/07-회의록목록조회및상세조회.puml b/design/backend/sequence/outer/07-회의록목록조회및상세조회.puml deleted file mode 100644 index 1d34425..0000000 --- a/design/backend/sequence/outer/07-회의록목록조회및상세조회.puml +++ /dev/null @@ -1,62 +0,0 @@ -@startuml -!theme mono - -title 회의록 목록 조회 및 상세 조회 Flow - -actor "User" as user -participant "Web App" as web -participant "API Gateway" as gateway -participant "Meeting Service" as meeting -participant "AI Service" as ai -participant "Redis" as redis -participant "PostgreSQL" as db - -== 회의록 목록 조회 == - -user -> web: 회의록 목록 화면 진입 -web -> gateway: GET /api/meetings/transcripts\n(filters: status, date, participants) -gateway -> meeting: 회의록 목록 조회 요청\n(필터 및 정렬 조건 포함) - -meeting -> redis: 캐시 조회\n(key: transcripts:filter:{hash}) -alt 캐시 히트 - redis --> meeting: 캐시된 목록 반환 -else 캐시 미스 - meeting -> db: SELECT transcripts\nWHERE status, date, participants\nORDER BY created_at - db --> meeting: 회의록 목록 데이터 - meeting -> redis: 캐시 저장\n(TTL: 5분) -end - -meeting --> gateway: 회의록 목록 응답 -gateway --> web: 200 OK\n{transcripts: [...]} -web --> user: 회의록 목록 표시\n(필터/정렬 적용) - -== 회의록 상세 조회 == - -user -> web: 회의록 항목 클릭 -web -> gateway: GET /api/meetings/transcripts/{id} -gateway -> meeting: 회의록 상세 조회 요청 - -meeting -> redis: 캐시 조회\n(key: transcript:{id}) -alt 캐시 히트 - redis --> meeting: 캐시된 상세 데이터 -else 캐시 미스 - meeting -> db: SELECT transcript, content,\nparticipants, action_items\nWHERE id = {id} - db --> meeting: 회의록 상세 데이터 - meeting -> redis: 캐시 저장\n(TTL: 10분) -end - -== 연관 회의록 자동 링크 == - -meeting ->> ai: 연관 회의록 요청\n(transcript_id, threshold: 0.7) -ai -> db: Vector 유사도 검색\n(임베딩 기반, similarity > 70%) -db --> ai: 유사 회의록 목록\n(최대 5개) -ai --> meeting: 연관 회의록 ID 목록 - -meeting -> db: 연관 회의록 메타데이터 조회 -db --> meeting: 연관 회의록 제목, 날짜, 참석자 - -meeting --> gateway: 회의록 상세 + 연관 목록 응답 -gateway --> web: 200 OK\n{transcript: {...},\nrelated: [...]} -web --> user: 회의록 상세 표시\n+ 연관 회의록 링크 - -@enduml diff --git a/design/backend/sequence/outer/08-맥락기반용어설명제공.puml b/design/backend/sequence/outer/08-맥락기반용어설명제공.puml deleted file mode 100644 index e631042..0000000 --- a/design/backend/sequence/outer/08-맥락기반용어설명제공.puml +++ /dev/null @@ -1,78 +0,0 @@ -@startuml -!theme mono - -title 맥락 기반 용어 설명 제공 Flow - -actor "User" as user -participant "Web App" as web -participant "API Gateway" as gateway -participant "RAG Service" as rag -participant "Vector DB" as vectordb -participant "PostgreSQL" as db - -== 실시간 용어 감지 == - -user -> web: 회의록 내용 조회 중 -web -> gateway: GET /api/meetings/transcripts/{id} -gateway -> rag: 회의록 내용 전달\n(기술 용어 감지 요청) - -rag -> rag: NLP 기반 기술 용어 추출\n(confidence > 70%) -note right of rag - - 도메인 용어 사전 매칭 - - 컨텍스트 분석 - - 신뢰도 점수 계산 -end note - -rag --> gateway: 감지된 용어 목록\n{terms: [{word, position, confidence}]} -gateway --> web: 회의록 + 하이라이트 용어 -web --> user: 회의록 표시\n(기술 용어 하이라이트) - -== 용어 설명 요청 == - -user -> web: 하이라이트 용어 호버 -web -> gateway: GET /api/rag/terms/explain\n?term={용어}&context={문장}&meeting_id={id} -gateway -> rag: 맥락 기반 용어 설명 요청 - -== Vector 유사도 검색 == - -rag -> vectordb: Vector 검색\n(term embedding, similarity search) -note right of vectordb - - 과거 회의록 임베딩 검색 - - 관련 문서 임베딩 검색 - - 유사도 임계값: 0.7 -end note - -vectordb --> rag: 유사 문서 조각 목록\n(최대 10개, similarity > 70%) - -rag -> db: 관련 컨텍스트 메타데이터 조회\n(project_id, issue_id, meeting_ids) -db --> rag: 프로젝트, 이슈, 회의 정보 - -== 설명 생성 == - -rag -> rag: LLM 기반 설명 생성 -note right of rag - 생성 내용: - 1. 간단한 정의 - 2. 현재 회의에서의 맥락적 의미 - 3. 관련 프로젝트/이슈 - 4. 과거 회의 참조 (최대 3개) -end note - -rag -> db: 과거 회의 제목 및 날짜 조회\n(meeting_ids, LIMIT 3) -db --> rag: 회의 메타데이터 - -rag --> gateway: 용어 설명 응답\n{definition, context_meaning,\nrelated_projects, past_meetings} -gateway --> web: 200 OK\n{explanation: {...}} -web --> user: 툴팁/사이드 패널에 설명 표시 -note left of user - 표시 내용: - - 📖 정의: "..." - - 💬 맥락: "이 회의에서는..." - - 🔗 관련: 프로젝트 A, 이슈 #123 - - 📅 과거 회의: - • 2025-08-15 기획 회의 - • 2025-07-20 기술 검토 - • 2025-06-10 아키텍처 논의 -end note - -@enduml diff --git a/design/backend/sequence/outer/09-Todo완료및회의록반영.puml b/design/backend/sequence/outer/09-Todo완료및회의록반영.puml deleted file mode 100644 index e306497..0000000 --- a/design/backend/sequence/outer/09-Todo완료및회의록반영.puml +++ /dev/null @@ -1,94 +0,0 @@ -@startuml -!theme mono - -title Flow 9: Todo 완료 및 회의록 반영 (Todo Completion and Transcript Update) - -actor User as user -participant "Web App" as web -participant "API Gateway" as gateway -participant "Todo Service" as todo -participant "RabbitMQ" as mq -participant "Meeting Service" as meeting -participant "Notification Service" as notification -participant "Redis" as redis -participant "PostgreSQL\n(Todo DB)" as tododb -participant "PostgreSQL\n(Meeting DB)" as meetingdb -participant "WebSocket\nConnection" as ws - -== Todo 완료 처리 (Todo Completion Processing) == - -user -> web: Todo 완료 버튼 클릭\n(Click "Complete" button) -web -> gateway: PUT /api/todos/{todoId}/complete\n(Todo 완료 요청) - -gateway -> todo: PUT /todos/{todoId}/complete\n(Todo 완료 처리 요청) -activate todo - -todo -> tododb: UPDATE todos\nSET status='COMPLETED',\ncompleted_at=NOW(),\ncompleted_by={userId}\nWHERE id={todoId} -tododb --> todo: 완료 상태 저장 완료\n(Completion status saved) - -todo -> redis: DEL cache:todo:{todoId}\n(Todo 캐시 무효화) -redis --> todo: 캐시 삭제 완료 - -todo --> gateway: 200 OK\n{todo: {..., status: "COMPLETED"}} -gateway --> web: 200 OK -web --> user: Todo 완료 표시\n(Show completion checkmark) - -== 비동기 이벤트 발행 (Async Event Publishing) == - -todo ->> mq: Publish "TodoCompleted"\n{todoId, meetingId, userId,\ncompletedAt} -deactivate todo - -mq ->> meeting: Consume "TodoCompleted"\n(Todo 완료 이벤트 수신) -activate meeting - -== 회의록 업데이트 (Meeting Transcript Update) == - -meeting -> redis: GET cache:transcript:{meetingId}\n(회의록 캐시 조회) -alt 캐시 히트 (Cache Hit) - redis --> meeting: 회의록 데이터 반환 -else 캐시 미스 (Cache Miss) - meeting -> meetingdb: SELECT * FROM transcripts\nWHERE meeting_id={meetingId} - meetingdb --> meeting: 회의록 데이터 반환 -end - -meeting -> meeting: Todo 섹션에 완료 상태 반영\n(Update todo section:\ncheckmark, completed_at,\ncompleted_by) - -meeting -> meetingdb: UPDATE transcripts\nSET content={updatedContent},\nupdated_at=NOW()\nWHERE meeting_id={meetingId} -meetingdb --> meeting: 회의록 업데이트 완료 - -meeting -> redis: DEL cache:transcript:{meetingId}\n(회의록 캐시 무효화) -redis --> meeting: 캐시 삭제 완료 - -== 알림 발송 (Notification Sending) == - -meeting ->> mq: Publish "TranscriptUpdated"\n{meetingId, updateType:\n"TODO_COMPLETED",\ntodoId, userId} -deactivate meeting - -mq ->> notification: Consume "TranscriptUpdated"\n(회의록 업데이트 이벤트 수신) -activate notification - -notification -> meetingdb: SELECT creator_id\nFROM transcripts\nWHERE meeting_id={meetingId} -meetingdb --> notification: 회의록 작성자 ID 반환 - -notification -> notification: 알림 메시지 생성\n("Todo가 완료되었습니다") - -notification ->> user: 회의록 작성자에게\n완료 알림 전송\n(Send completion notification) - -deactivate notification - -== 실시간 동기화 (Real-time Sync) == - -mq ->> ws: Consume "TranscriptUpdated"\n(실시간 동기화 이벤트) -activate ws - -ws -> redis: GET ws:participants:{meetingId}\n(현재 회의록 조회 중인\n참여자 목록 조회) -redis --> ws: 참여자 WebSocket 세션 목록 - -loop 각 참여자에게 (For each participant) - ws ->> web: WebSocket Push\n{type: "TODO_COMPLETED",\ntodoId, completedAt,\ncompletedBy} - web -> web: 화면에 완료 상태 실시간 반영\n(Update UI with completion status) -end - -deactivate ws - -@enduml diff --git a/design/backend/sequence/outer/README.md b/design/backend/sequence/outer/README.md deleted file mode 100644 index 723e177..0000000 --- a/design/backend/sequence/outer/README.md +++ /dev/null @@ -1,328 +0,0 @@ -# 외부 시퀀스 설계서 (Outer Sequence Design) - -## 개요 - -본 문서는 회의록 작성 및 공유 개선 서비스의 **외부 시퀀스 다이어그램**을 설명합니다. -외부 시퀀스는 **마이크로서비스 간의 통신 흐름**을 나타내며, 주요 사용자 플로우별로 서비스 간 상호작용을 정의합니다. - -### 문서 정보 -- **작성일**: 2025-10-22 -- **작성자**: 홍길동 (Architect) -- **버전**: 2.0 -- **설계 원칙**: 공통설계원칙 및 외부시퀀스설계가이드 준용 - -### 병렬 처리 전략 적용 -- **Group A (순차)**: 회의 생애주기 플로우 (플로우 10-13) -- **Group B (독립)**: Todo 관리 플로우 (플로우 14) -- **Group C (독립)**: 회의록 조회/수정 플로우 (플로우 15) -- 3개 서브 에이전트가 병렬로 작업하여 전체 작업 시간 약 60% 단축 - ---- - -## 외부 시퀀스 목록 - -### 1. 사용자 인증 및 대시보드 조회 -- **파일**: [01-사용자인증및대시보드조회.puml](./01-사용자인증및대시보드조회.puml) -- **관련 유저스토리**: AFR-USER-010, AFR-USER-020 -- **참여 서비스**: - - Web App - - API Gateway - - User Service - - Meeting Service - - Todo Service - - Redis (Cache) - - PostgreSQL (User DB, Meeting DB, Todo DB) - -**주요 흐름**: -1. **사용자 인증**: - - 사용자가 사번과 비밀번호로 로그인 - - User Service가 LDAP 연동하여 인증 - - JWT 토큰 생성 및 사용자 프로필 Redis 캐싱 (TTL: 30분) - - 인증 성공 시 토큰 반환 - -2. **대시보드 데이터 조회**: - - 예정된 회의 조회 (Meeting Service) - - Redis 캐시 확인 → Cache Miss → DB 조회 → 캐싱 (TTL: 10분) - - 진행 중 Todo 조회 (Todo Service) - - Redis 캐시 확인 → Cache Hit → 캐시에서 직접 반환 - - 최근 회의록 조회 (Meeting Service) - - Redis 캐시 확인 → Cache Miss → DB 조회 → 캐싱 (TTL: 10분) - - Todo 통계 조회 (Todo Service) - - Redis 캐시 확인 → Cache Hit → 캐시에서 직접 반환 - -**적용 패턴**: -- **Cache-Aside Pattern**: 캐시 우선 조회로 DB 부하 감소 -- **JWT 인증**: API Gateway에서 일괄 토큰 검증 -- **LDAP 연동**: 기업 사용자 인증 - -**성능 목표**: -- API 응답 시간 (P95): < 500ms -- Cache Hit Rate: > 70% - ---- - -### 2. 회의 예약 및 초대 -- **파일**: [02-회의예약및초대.puml](./02-회의예약및초대.puml) -- **관련 유저스토리**: UFR-MEET-010 -- **참여 서비스**: - - Web App - - API Gateway - - Meeting Service - - User Service - - Notification Service - - RabbitMQ (Message Broker) - - Redis (Cache) - - PostgreSQL (Meeting DB, User DB, Notification DB) - -**주요 흐름**: -1. **회의 예약**: - - 사용자가 회의 정보 입력 (제목, 날짜/시간, 장소, 참석자) - - 입력 유효성 검증 (필수 필드, 날짜 형식, 이메일 형식) - - Meeting Service가 회의 생성 및 DB 저장 - - 회의 정보 Redis 캐싱 (TTL: 10분) - -2. **이벤트 발행**: - - Meeting Service가 `MeetingCreated` 이벤트를 RabbitMQ에 발행 - - Exchange: `meeting.events` - - Routing Key: `meeting.created` - - Notification Service가 이벤트 구독 - -3. **초대 알림 발송**: - - Notification Service가 `MeetingCreated` 이벤트 수신 - - User Service에서 참석자 정보 조회 (캐시 우선) - - 알림 메시지 생성 및 DB 저장 - - 각 참석자에게 이메일 초대 발송 - - 알림 상태 업데이트 (PENDING → SENT) - - 리마인더 일정 생성 (회의 시작 30분 전) - -4. **결과 확인**: - - 사용자가 대시보드에서 새로 예약한 회의 확인 - - 캐시 무효화 후 DB 재조회 및 재캐싱 - -**적용 패턴**: -- **Publisher-Subscriber Pattern**: 이벤트 기반 느슨한 결합 -- **Queue-Based Load Leveling**: 대량 알림 발송 시 부하 분산 -- **Cache-Aside Pattern**: 사용자 정보 캐싱으로 성능 향상 -- **Asynchronous Processing**: 알림 발송은 비동기로 처리 - -**이벤트 스키마**: -```json -{ - "eventType": "MeetingCreated", - "payload": { - "meetingId": "uuid", - "title": "프로젝트 킥오프 회의", - "dateTime": "2025-02-01T14:00:00", - "location": "회의실 A", - "creatorId": "userId", - "participants": ["user1@company.com", "user2@company.com"] - } -} -``` - -**성능 목표**: -- 회의 생성 응답 시간: < 300ms -- 알림 발송 지연 시간: < 5초 - ---- - -## 설계 원칙 - -### 1. 통신 패턴 - -#### 동기 통신 (REST API) -- **적용 대상**: 즉시 응답이 필요한 단순 조회 -- **특징**: - - API Gateway를 통한 일관된 인증/인가 - - 캐시 우선 전략으로 직접 의존성 최소화 - -#### 비동기 통신 (Message Queue) -- **적용 대상**: 이벤트 기반 통신, 느슨한 결합 -- **특징**: - - RabbitMQ Topic Exchange를 통한 Pub/Sub 패턴 - - 이벤트 발행자와 구독자 간 느슨한 결합 - - 대량 작업 시 Queue-Based Load Leveling - -### 2. 캐싱 전략 (Cache-Aside) - -| 데이터 유형 | TTL | 캐시 키 패턴 | 무효화 시점 | -|------------|-----|--------------|------------| -| 사용자 프로필 | 30분 | `user:profile:{userId}` | 프로필 수정 시 | -| 사용자 권한 | 1시간 | `user:auth:{userId}` | 권한 변경 시 | -| 회의 정보 | 10분 | `meeting:info:{meetingId}` | 회의 수정 시 | -| 회의 참여자 | 10분 | `meeting:participants:{meetingId}` | 참여자 변경 시 | -| Todo 목록 | 5분 | `todo:user:{userId}` | Todo 상태 변경 시 | -| Todo 통계 | 5분 | `todo:stats:{userId}` | Todo 완료 시 | - -**캐시 처리 플로우**: -1. 조회 요청 → Redis 캐시 확인 -2. Cache Hit → 캐시 데이터 반환 -3. Cache Miss → DB 조회 → Redis 저장 (TTL 설정) → 데이터 반환 -4. 데이터 수정 시 → DB 업데이트 → Redis 캐시 무효화 - -### 3. 이벤트 기반 아키텍처 - -#### RabbitMQ Exchange/Queue 구성 -- **Topic Exchange**: `meeting.events`, `transcript.events`, `todo.events` -- **Queue 네이밍**: `{service}.{event-category}.queue` -- **Routing Key 패턴**: `{event}.{action}` (예: `meeting.created`, `todo.completed`) - -#### 주요 이벤트 - -| 이벤트 | 발행자 | 구독자 | 목적 | -|--------|--------|--------|------| -| **MeetingCreated** | Meeting Service | Notification Service
AI Service | 참석자 알림 발송
회의 분석 준비 | -| **MeetingEnded** | Meeting Service | STT Service
AI Service
Todo Service
Notification Service | 회의록 생성
AI 분석 시작
Todo 추출
종료 알림 | -| **TodoCreated** | Todo Service | Notification Service | 담당자 알림 발송 | - ---- - -## 다이어그램 렌더링 방법 - -### PlantUML 렌더링 - -#### 1. Visual Studio Code 플러그인 사용 -1. PlantUML 확장 설치: `jebbs.plantuml` -2. `.puml` 파일 열기 -3. `Alt + D` (미리보기) 또는 우클릭 → `Preview Current Diagram` - -#### 2. 온라인 렌더링 -- [PlantUML Online Editor](https://www.plantuml.com/plantuml/uml/) -- 파일 내용 복사 → 붙여넣기 → 렌더링 - -#### 3. 로컬 PlantUML 서버 -```bash -# Docker로 PlantUML 서버 실행 -docker run -d --name plantuml -p 38080:8080 plantuml/plantuml-server:latest - -# 브라우저에서 접근 -http://localhost:38080 -``` - ---- - -## 다음 단계 - -### 1. 내부 시퀀스 설계 -각 서비스 내부의 상세 처리 흐름을 설계합니다: -- User Service 내부 시퀀스 (인증 처리, 프로필 관리) -- Meeting Service 내부 시퀀스 (회의 생성, 회의록 관리) -- Notification Service 내부 시퀀스 (알림 발송, 리마인더 관리) - -### 2. API 명세 작성 -각 서비스별 REST API 명세를 OpenAPI 3.0 형식으로 작성합니다. - -### 3. 클래스 설계 -엔티티, DTO, 서비스 클래스 설계를 진행합니다. - -### 4. 데이터 설계 -각 서비스별 데이터베이스 스키마 설계를 진행합니다. - ---- - -## 참고 자료 - -- [논리 아키텍처 설계서](../../logical/logical-architecture.md) -- [유저스토리](../../../userstory.md) -- [아키텍처 패턴 적용 방안](../../../pattern/architecture-pattern.md) -- [클라우드 디자인 패턴 요약](../../../../claude/cloud-design-patterns.md) - ---- - ---- - -## 신규 플로우 (v2.0) - -### 10. 회의 예약 및 초대 -- **파일**: [회의예약및초대.puml](./회의예약및초대.puml) -- **관련 유저스토리**: UFR-MEET-010 -- **참여 서비스**: Frontend, API Gateway, Meeting Service, User Service, Redis, RabbitMQ, Notification Service, Meeting DB -- **주요 특징**: - - Cache-Aside 패턴으로 참석자 정보 조회 (Redis 캐시 우선) - - 회의 정보 저장 및 캐싱 (TTL: 10분) - - MeetingCreated 이벤트 비동기 발행 - - Notification Service가 참석자 전원에게 초대 이메일 발송 - -### 11. 회의 시작 및 회의록 작성 -- **파일**: [회의시작및회의록작성.puml](./회의시작및회의록작성.puml) -- **관련 유저스토리**: UFR-MEET-030, UFR-STT-010/020, UFR-AI-010, UFR-RAG-010/020, UFR-COLLAB-010 -- **참여 서비스**: Frontend, API Gateway, Meeting Service, STT Service, AI Service, RAG Service, Collaboration Service, Redis, RabbitMQ, Azure Speech, LLM Server, 각 서비스 DB -- **주요 특징**: - - 실시간 음성 인식 (Azure Speech, 5초 간격) - - AI 기반 회의록 자동 작성 (LLM) - - WebSocket 실시간 동기화 (델타만 전송) - - RAG 기반 전문용어 감지 및 맥락 설명 제공 - - 가장 복잡한 플로우 (동기+비동기+실시간 혼합) - -### 12. 회의 종료 및 Todo 추출 -- **파일**: [회의종료및Todo추출.puml](./회의종료및Todo추출.puml) -- **관련 유저스토리**: UFR-MEET-040/050, UFR-AI-020, UFR-TODO-010 -- **참여 서비스**: Frontend, API Gateway, Meeting Service, AI Service, Todo Service, Notification Service, Redis, RabbitMQ, LLM Server, 각 서비스 DB -- **주요 특징**: - - 회의 통계 자동 생성 (duration, 참석자 수, 발언 수) - - AI Todo 자동 추출 (액션 아이템 식별, 담당자 자동 지정) - - 이벤트 체인: MeetingEnded → TodoExtracted → TodoCreated - - Todo 목록 캐싱 (TTL: 5분) - - 담당자에게 자동 알림 발송 - -### 13. 회의록 확정 및 공유 -- **파일**: [회의록확정및공유.puml](./회의록확정및공유.puml) -- **관련 유저스토리**: UFR-MEET-060 -- **참여 서비스**: Frontend, API Gateway, Meeting Service, Notification Service, Redis, RabbitMQ, Meeting DB -- **주요 특징**: - - 필수 항목 자동 검사 (제목, 참석자, 논의 내용, 결정 사항) - - 고유 공유 링크 생성 (UUID 기반) - - 공유 보안 설정 (비밀번호, 유효기간) - - TranscriptShared 이벤트 발행 - - 다음 회의 일정 자동 캘린더 등록 (선택) - -### 14. Todo 완료 및 회의록 반영 -- **파일**: [Todo완료및회의록반영.puml](./Todo완료및회의록반영.puml) -- **관련 유저스토리**: UFR-TODO-030 -- **참여 서비스**: Frontend, API Gateway, Todo Service, Meeting Service, Notification Service, Redis, RabbitMQ, 각 서비스 DB -- **주요 특징** (**차별화 포인트**): - - Todo 완료가 회의록에 실시간 반영되는 양방향 연동 - - 완료 시간 및 완료자 정보 자동 기록 - - TodoCompleted 이벤트 → Meeting Service가 회의록 자동 업데이트 - - 모든 Todo 완료 시 전체 완료 알림 자동 발송 - - Cache-Aside 패턴으로 회의록 조회 최적화 - -### 15. 회의록 조회 및 수정 -- **파일**: [회의록조회및수정.puml](./회의록조회및수정.puml) -- **관련 유저스토리**: UFR-MEET-046/047/055, UFR-COLLAB-010 -- **참여 서비스**: Frontend, API Gateway, Meeting Service, Collaboration Service, Redis, Meeting DB -- **주요 특징**: - - Cache-Aside 패턴으로 목록/상세 조회 (TTL: 10분) - - 권한 검증 (본인 작성 회의록만 수정 가능) - - 수정 이력 자동 기록 (수정자, 수정 시간, 변경 내용) - - WebSocket 실시간 동기화 (델타만 전송) - - 버전 관리 (새 버전 생성, 이전 버전 보관) - - 확정완료 → 작성중 자동 상태 변경 - ---- - -## 설계 원칙 적용 - -### PlantUML 문법 검사 -✅ **모든 파일 검증 통과** (Docker 기반 PlantUML 검사 완료) -- `!theme mono` 적용 -- 동기: 실선 (→), 비동기: 점선 (->>) 명확 구분 -- `..>` 사용 금지 (sequence diagram 원칙 준수) - -### 논리 아키텍처 일치성 -- ✅ 참여자: 논리 아키텍처의 서비스, DB, 메시지 큐 구조 일치 -- ✅ 통신 방식: 동기(REST), 비동기(RabbitMQ), 실시간(WebSocket) 일치 -- ✅ 캐시 전략: Cache-Aside 패턴, TTL 설정 일치 - -### 유저스토리 커버리지 -- ✅ 신규 6개 플로우가 11개 유저스토리 완전 커버 -- ✅ 기존 9개 플로우와 통합하여 전체 시스템 커버 - ---- - -## 문서 이력 - -| 버전 | 작성일 | 작성자 | 변경 내용 | -|------|--------|--------|----------| -| 1.0 | 2025-01-22 | 길동 (Architect) | 초안 작성 (Flow 1-9) | -| 2.0 | 2025-10-22 | 길동 (Architect) | 신규 6개 플로우 추가 (Flow 10-15), 병렬 처리 전략 적용 | diff --git a/design/backend/sequence/outer/Todo완료및회의록반영.puml b/design/backend/sequence/outer/Todo완료및회의록반영.puml deleted file mode 100644 index 4e39185..0000000 --- a/design/backend/sequence/outer/Todo완료및회의록반영.puml +++ /dev/null @@ -1,211 +0,0 @@ -@startuml Todo완료및회의록반영 -!theme mono - -title 외부 시퀀스 다이어그램: Todo 완료 및 회의록 반영\n(Flow 5: Todo Completion and Meeting Minutes Reflection) - -' 참여자 정의 -actor "사용자\n(Todo 담당자)" as User -participant "Frontend" as FE -participant "API Gateway" as GW -participant "Todo Service" as TodoSvc -participant "Meeting Service" as MeetingSvc -participant "Notification Service" as NotifySvc -database "Todo DB" as TodoDB -database "Meeting DB" as MeetingDB -queue "RabbitMQ" as MQ -database "Redis Cache" as Redis - -== Todo 완료 요청 == - -User -> FE: Todo 완료 버튼 클릭 -activate FE - -FE -> FE: 완료 여부 확인 다이얼로그 표시 -FE --> User: 확인 요청 표시 -User -> FE: 완료 확인 - -FE -> GW: PUT /todos/{todoId}/complete -activate GW -note right: JWT 인증 및 권한 검증 - -GW -> TodoSvc: PUT /todos/{todoId}/complete -activate TodoSvc - -== Todo 완료 처리 == - -TodoSvc -> TodoDB: Todo 상태 업데이트\n(상태=완료, 완료시간, 완료자 기록) -activate TodoDB -TodoDB --> TodoSvc: 업데이트 완료 -deactivate TodoDB - -note right of TodoSvc - 완료 처리 정보: - - 완료 시간 자동 기록 - - 완료자 정보 저장 - - 완료 상태로 변경 -end note - -== 캐시 무효화 == - -TodoSvc -> Redis: DEL todo:user:{userId} -activate Redis -Redis --> TodoSvc: 캐시 삭제 완료 -deactivate Redis - -TodoSvc -> Redis: DEL todo:stats:{userId} -activate Redis -Redis --> TodoSvc: 캐시 삭제 완료 -deactivate Redis - -note right of Redis - 캐시 무효화 대상: - - todo:user:{userId} - - todo:stats:{userId} -end note - -== TodoCompleted 이벤트 발행 == - -TodoSvc -> MQ: publish TodoCompleted\n(todoId, meetingId, userId, completedAt) -activate MQ - -note right of MQ - 이벤트 내용: - - todoId: Todo ID - - meetingId: 관련 회의 ID - - userId: 완료자 ID - - completedAt: 완료 시간 - - sectionId: 회의록 섹션 ID -end note - -TodoSvc --> GW: 200 OK\n(완료 처리 결과 반환) -deactivate TodoSvc -GW --> FE: 200 OK -deactivate GW - -FE -> FE: Todo 완료 상태 UI 업데이트 -FE --> User: 완료 처리 완료 표시 -deactivate FE - -== Meeting Service: TodoCompleted 이벤트 구독 == - -MQ ->> MeetingSvc: TodoCompleted 이벤트 수신 -activate MeetingSvc - -note right of MeetingSvc - 비동기 처리: - 구독자가 이벤트 수신 -end note - -MeetingSvc -> Redis: GET meeting:info:{meetingId} -activate Redis - -alt 캐시 Hit - Redis --> MeetingSvc: 회의 정보 반환 -else 캐시 Miss - Redis --> MeetingSvc: null - MeetingSvc -> MeetingDB: SELECT 회의 정보 - activate MeetingDB - MeetingDB --> MeetingSvc: 회의 정보 반환 - deactivate MeetingDB - - MeetingSvc -> Redis: SETEX meeting:info:{meetingId}\n(TTL: 10분) - Redis --> MeetingSvc: 캐시 저장 완료 -end - -deactivate Redis - -note right of MeetingSvc - Cache-Aside 패턴 적용: - 1. 캐시 조회 시도 - 2. 캐시 미스 시 DB 조회 - 3. 조회 결과 캐시 저장 -end note - -== 회의록에 완료 상태 자동 반영 == - -MeetingSvc -> MeetingDB: UPDATE 회의록 Todo 섹션\n(완료 상태, 완료 시간, 완료자) -activate MeetingDB - -note right of MeetingDB - 회의록 반영 내용: - - Todo 섹션에 완료 표시 (✅) - - 완료 시간 기록 - - 완료자 정보 표시 - - 양방향 연결 유지 -end note - -MeetingDB --> MeetingSvc: 업데이트 완료 -deactivate MeetingDB - -== 캐시 무효화 == - -MeetingSvc -> Redis: DEL meeting:info:{meetingId} -activate Redis -Redis --> MeetingSvc: 캐시 삭제 완료 -deactivate Redis - -note right of MeetingSvc - 회의록 수정 시 캐시 무효화: - 다음 조회 시 최신 정보 반영 -end note - -== TranscriptUpdated 이벤트 발행 (선택) == - -MeetingSvc -> MQ: publish TranscriptUpdated\n(meetingId, updateType=TODO_COMPLETED) -note right of MeetingSvc - 선택적 이벤트: - 실시간 협업 중인 경우 - 다른 참석자에게 알림 가능 -end note - -deactivate MeetingSvc - -== Notification Service: TodoCompleted 이벤트 구독 == - -MQ ->> NotifySvc: TodoCompleted 이벤트 수신 -activate NotifySvc - -NotifySvc -> NotifySvc: 알림 내용 생성 -note right of NotifySvc - 알림 내용: - - Todo 완료 알림 - - 회의록 작성자에게 발송 - - 완료자 정보 포함 -end note - -NotifySvc -> NotifySvc: 회의록 알림 발송\n(이메일) -note right of NotifySvc - 알림 발송 대상: - - 회의록 작성자 - - 참석자 (선택) -end note - -== 모든 Todo 완료 확인 == - -NotifySvc -> TodoDB: SELECT COUNT(*)\nWHERE meetingId={meetingId}\nAND status != COMPLETED -activate TodoDB -TodoDB --> NotifySvc: 미완료 Todo 개수 반환 -deactivate TodoDB - -alt 모든 Todo 완료 - NotifySvc -> NotifySvc: 전체 완료 알림 생성 - note right of NotifySvc - 전체 완료 알림: - - "모든 Todo가 완료되었습니다" - - 회의록 작성자 및 참석자 전원 발송 - end note - NotifySvc -> NotifySvc: 전체 완료 알림 발송\n(이메일) -end - -deactivate NotifySvc -deactivate MQ - -note over User, Redis - 차별화 포인트: - - Todo 완료가 회의록에 실시간 반영되어 양방향 연동 - - 회의 결과 추적 용이 - - 완료 시간 및 완료자 정보 자동 기록 - - 모든 Todo 완료 시 전체 완료 알림 자동 발송 -end note - -@enduml diff --git a/design/backend/sequence/outer/회의록조회및수정.png b/design/backend/sequence/outer/회의록조회및수정.png deleted file mode 100644 index c29396b67a57f0678504dbc9dccb6fde4571a129..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41927 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 diff --git a/design/backend/sequence/outer/회의록조회및수정.puml b/design/backend/sequence/outer/회의록조회및수정.puml deleted file mode 100644 index 5bb9164..0000000 --- a/design/backend/sequence/outer/회의록조회및수정.puml +++ /dev/null @@ -1,123 +0,0 @@ -@startuml 회의록조회및수정 -!theme mono - -title 플로우 6: 회의록 조회 및 수정 - -actor "Frontend" as FE -participant "API Gateway" as GW -participant "Meeting Service" as MS -participant "Collaboration Service" as CS -participant "Redis" as RD -database "Meeting DB" as MDB - -== 회의록 목록 조회 == -FE -> GW: 회의록 목록 조회 요청\n(필터/정렬/검색 조건) -activate GW -GW -> MS: 목록 조회 요청 -activate MS - -MS -> RD: 캐시 조회 (Cache-Aside)\nKey: meeting:list:{userId}:{filter} -activate RD - -alt Cache Hit - RD --> MS: 캐시 데이터 반환 - MS --> GW: 목록 데이터 반환 - GW --> FE: 목록 표시 -else Cache Miss - RD --> MS: 캐시 없음 - deactivate RD - MS -> MDB: DB 조회\n(필터, 정렬, 검색 조건 적용) - activate MDB - MDB --> MS: 목록 데이터 - deactivate MDB - MS -> RD: 캐시 저장 (TTL 10분)\nKey: meeting:list:{userId}:{filter} - activate RD - RD --> MS: 저장 완료 - deactivate RD - MS --> GW: 목록 데이터 반환 - GW --> FE: 목록 표시 -end - -deactivate MS -deactivate GW - -== 회의록 상세 조회 == -FE -> GW: 회의록 클릭\n회의록 상세 조회 요청 -activate GW -GW -> MS: 상세 조회 요청 (meetingId) -activate MS - -MS -> RD: 캐시 조회 (Cache-Aside)\nKey: meeting:info:{meetingId} -activate RD - -alt Cache Hit - RD --> MS: 캐시 데이터 반환\n(회의 정보, 섹션별 내용, 관련 회의록) - MS --> GW: 상세 데이터 반환 - GW --> FE: 상세 정보 표시 -else Cache Miss - RD --> MS: 캐시 없음 - deactivate RD - MS -> MDB: DB 조회\n(회의 정보, 섹션별 내용, 관련 회의록) - activate MDB - MDB --> MS: 상세 데이터 - deactivate MDB - MS -> RD: 캐시 저장 (TTL 10분)\nKey: meeting:info:{meetingId} - activate RD - RD --> MS: 저장 완료 - deactivate RD - MS --> GW: 상세 데이터 반환 - GW --> FE: 상세 정보 표시 -end - -deactivate MS -deactivate GW - -== 회의록 수정 == -FE -> GW: 수정 버튼 클릭 -activate GW -GW -> MS: 권한 확인 요청 -activate MS -MS -> MS: 수정 권한 검증\n(본인 작성 회의록만 수정 가능) - -alt 권한 없음 - MS --> GW: 권한 오류 - GW --> FE: 수정 불가 알림 -else 권한 있음 - MS --> GW: 권한 확인 완료 - GW --> FE: 수정 모드 진입 - deactivate GW - - FE -> GW: 회의록 수정 내용 전송 - activate GW - GW -> MS: 수정 요청 - - MS -> MDB: 수정 내용 저장\n수정 이력 기록\n(수정자, 수정 시간, 변경 내용) - activate MDB - MDB --> MS: 저장 완료 - deactivate MDB - - MS -> RD: 캐시 무효화\nKey: meeting:info:{meetingId} - activate RD - RD --> MS: 무효화 완료 - deactivate RD - - MS ->> CS: 실시간 동기화 요청\n(비동기) - activate CS - CS ->> FE: WebSocket: 모든 참석자에게\n수정 델타 전송\n수정 영역 하이라이트 (3초간) - deactivate CS - - MS -> MDB: 새 버전 번호 생성\n이전 버전 보관 - activate MDB - MDB --> MS: 버전 관리 완료 - deactivate MDB - - note over MS: 확정완료 상태였던 경우\n작성중 상태로 변경 - - MS --> GW: 수정 완료 응답 - GW --> FE: 수정 완료 표시 -end - -deactivate MS -deactivate GW - -@enduml diff --git a/design/backend/sequence/outer/회의록확정및공유.puml b/design/backend/sequence/outer/회의록확정및공유.puml deleted file mode 100644 index 0e17357..0000000 --- a/design/backend/sequence/outer/회의록확정및공유.puml +++ /dev/null @@ -1,243 +0,0 @@ -@startuml 회의록확정및공유 -!theme mono - -title 회의록 확정 및 공유 플로우 (UFR-MEET-060) - -actor "사용자" as User -participant "Frontend" as FE -participant "API Gateway" as GW -participant "Meeting Service" as MS -participant "Notification Service" as NS -participant "Redis" as Cache -database "Meeting DB" as MDB -queue "RabbitMQ" as MQ - -== 회의록 확정 == -User -> FE: 회의록 확정 버튼 클릭 -activate FE - -FE -> GW: POST /api/meetings/{meetingId}/minutes/finalize -activate GW - -GW -> MS: 회의록 확정 요청 -activate MS - -MS -> MS: 필수 항목 검사\n(제목, 참석자, 논의 내용, 결정 사항) - -alt 필수 항목 누락 - MS --> GW: 400 Bad Request\n{error: "필수 항목 누락", missingFields} - deactivate MS - GW --> FE: 오류 반환 - deactivate GW - FE --> User: 필수 항목 누락 안내 - deactivate FE -else 필수 항목 완료 - MS -> MDB: INSERT INTO minutes_versions\n(확정 버전 생성, 확정 시간 기록)\nUPDATE meetings SET minutes_status = 'finalized' - activate MDB - MDB --> MS: 확정 버전 ID 반환 - deactivate MDB - - MS -> Cache: DEL meeting:info:{meetingId}\n(캐시 무효화) - activate Cache - Cache --> MS: OK - deactivate Cache - - MS --> GW: 200 OK\n{versionId, status: "finalized"} - deactivate MS - - GW --> FE: 회의록 확정 완료 - deactivate GW - - FE --> User: 회의록 확정 완료 화면 표시 - deactivate FE -end - -== 회의록 공유 설정 == -User -> FE: 공유 설정 입력\n(대상: 참석자/전체/특정인, 권한: 읽기/편집, 방식: 링크/이메일) -activate FE - -FE -> GW: POST /api/meetings/{meetingId}/share\n{targets, permission, method, password, expiresAt} -activate GW - -GW -> MS: 공유 설정 요청 -activate MS - -MS -> MS: 고유 공유 링크 생성\n(UUID 기반 URL) -MS -> MDB: INSERT INTO share_settings\n(공유 대상, 권한, 링크, 비밀번호, 유효기간 저장) -activate MDB -MDB --> MS: 공유 설정 ID 반환 -deactivate MDB - -MS -> Cache: SET share:link:{shareId}\n(공유 링크 정보 캐싱, TTL: 유효기간) -activate Cache -Cache --> MS: OK -deactivate Cache - -MS ->> MQ: TranscriptShared 이벤트 발행\n{meetingId, shareId, targets, shareUrl, method} -activate MQ -note right of MQ - 이벤트: TranscriptShared - Routing Key: transcript.shared -end note -MQ -->> MS: ACK -deactivate MQ - -MS --> GW: 200 OK\n{shareUrl, shareId, expiresAt} -deactivate MS - -GW --> FE: 공유 링크 반환 -deactivate GW - -FE --> User: 공유 링크 및 설정 표시 -deactivate FE - -== 공유 알림 발송 (비동기) == -MQ ->> NS: TranscriptShared 이벤트 전달 -activate NS -note right of NS - 구독: transcript.shared - 큐: notification.transcript.shared -end note - -alt 공유 방식: 이메일 - loop 각 대상 - NS -> NS: 이메일 템플릿 생성\n(회의 제목, 공유 링크, 유효기간, 비밀번호) - NS -> NS: 이메일 발송 - end -else 공유 방식: 링크만 - NS -> NS: 알림 생성\n(링크 복사 완료) -end - -NS -->> MQ: ACK -deactivate NS - -== 다음 회의 일정 자동 등록 (선택) == -opt 회의록에 다음 회의 언급 존재 - User -> FE: 다음 회의 일정 자동 등록 확인 - activate FE - - FE -> GW: POST /api/meetings/{meetingId}/schedule-next - activate GW - - GW -> MS: 다음 회의 일정 등록 요청 - activate MS - - MS -> MDB: SELECT next_meeting_info\nFROM minutes\nWHERE meeting_id = {meetingId} - activate MDB - MDB --> MS: 다음 회의 정보 반환\n{suggestedDate, suggestedTime, suggestedAttendees} - deactivate MDB - - MS -> MS: 캘린더 등록 데이터 생성\n(제목, 날짜, 시간, 참석자) - MS -> MDB: INSERT INTO meetings\n(다음 회의 생성, 이전 회의 ID 링크) - activate MDB - MDB --> MS: 다음 회의 ID 반환 - deactivate MDB - - MS ->> MQ: MeetingCreated 이벤트 발행\n(다음 회의 초대) - activate MQ - MQ -->> MS: ACK - deactivate MQ - - MS --> GW: 200 OK\n{nextMeetingId, status: "scheduled"} - deactivate MS - - GW --> FE: 다음 회의 등록 완료 - deactivate GW - - FE --> User: 다음 회의 등록 완료 표시 - deactivate FE -end - -== 공유 링크 접근 (외부 사용자) == -actor "외부 사용자" as External -External -> FE: 공유 링크 접근\n(GET /share/{shareId}) -activate FE - -FE -> GW: 공유 링크 검증 요청 -activate GW - -GW -> MS: 공유 설정 조회 -activate MS - -MS -> Cache: GET share:link:{shareId} -activate Cache - -alt Cache Hit - Cache --> MS: 공유 설정 반환 -else Cache Miss - Cache --> MS: null - MS -> MDB: SELECT * FROM share_settings\nWHERE share_id = {shareId}\nAND expires_at > NOW() - activate MDB - - alt 유효한 공유 링크 - MDB --> MS: 공유 설정 반환 - deactivate MDB - - MS -> Cache: SET share:link:{shareId}\n(TTL: 남은 유효기간) - Cache --> MS: OK - else 만료 또는 존재하지 않음 - MDB --> MS: null - deactivate MDB - Cache --> MS: null - MS --> GW: 404 Not Found\n{error: "공유 링크가 만료되었거나 존재하지 않습니다"} - deactivate MS - GW --> FE: 오류 반환 - deactivate GW - FE --> External: 오류 화면 표시 - deactivate FE - end -end -deactivate Cache - -alt 비밀번호 설정된 경우 - MS --> GW: 비밀번호 요청 - deactivate MS - GW --> FE: 비밀번호 입력 화면 - deactivate GW - - External -> FE: 비밀번호 입력 - FE -> GW: 비밀번호 검증 요청 - activate GW - - GW -> MS: 비밀번호 검증 - activate MS - - alt 비밀번호 일치 - MS -> MDB: SELECT minutes\nFROM meetings\nWHERE meeting_id = {meetingId} - activate MDB - MDB --> MS: 회의록 반환 - deactivate MDB - - MS --> GW: 200 OK\n{minutes, permission} - deactivate MS - - GW --> FE: 회의록 데이터 반환 - deactivate GW - - FE --> External: 회의록 표시\n(권한에 따라 읽기/편집) - deactivate FE - else 비밀번호 불일치 - MS --> GW: 401 Unauthorized\n{error: "비밀번호가 일치하지 않습니다"} - deactivate MS - GW --> FE: 오류 반환 - deactivate GW - FE --> External: 오류 화면 표시 - deactivate FE - end -else 비밀번호 미설정 - MS -> MDB: SELECT minutes\nFROM meetings\nWHERE meeting_id = {meetingId} - activate MDB - MDB --> MS: 회의록 반환 - deactivate MDB - - MS --> GW: 200 OK\n{minutes, permission} - deactivate MS - - GW --> FE: 회의록 데이터 반환 - deactivate GW - - FE --> External: 회의록 표시\n(권한에 따라 읽기/편집) - deactivate FE -end - -@enduml diff --git a/design/backend/sequence/outer/회의시작및회의록작성.puml b/design/backend/sequence/outer/회의시작및회의록작성.puml deleted file mode 100644 index 52a968f..0000000 --- a/design/backend/sequence/outer/회의시작및회의록작성.puml +++ /dev/null @@ -1,170 +0,0 @@ -@startuml 회의시작및회의록작성 -!theme mono - -title 회의 시작 및 회의록 작성 플로우 (UFR-MEET-030, UFR-STT-010/020, UFR-AI-010, UFR-RAG-010/020, UFR-COLLAB-010) - -actor "참석자" as User -participant "Frontend" as FE -participant "API Gateway" as GW -participant "Meeting Service" as MS -participant "STT Service" as STT -participant "AI Service" as AI -participant "RAG Service" as RAG -participant "Collaboration Service" as CS -participant "Redis" as Cache -database "Meeting DB" as MDB -database "STT DB" as STTDB -database "AI DB" as AIDB -database "RAG DB" as RAGDB -queue "RabbitMQ" as MQ -participant "Azure Speech" as Azure -participant "LLM Server" as LLM - -== 회의 시작 == -User -> FE: 회의 시작 버튼 클릭 -activate FE - -FE -> GW: POST /api/meetings/{meetingId}/start -activate GW - -GW -> MS: 회의 시작 요청 -activate MS - -MS -> MDB: UPDATE meetings\nSET start_time = NOW(), status = 'in_progress'\n세션 생성 -activate MDB -MDB --> MS: 회의 시작 시간 기록 완료 -deactivate MDB - -MS ->> MQ: MeetingStarted 이벤트 발행\n{meetingId, sessionId, startTime} -activate MQ -note right of MQ - 이벤트: MeetingStarted - Routing Key: meeting.started -end note -MQ -->> MS: ACK -deactivate MQ - -MS --> GW: 200 OK\n{sessionId, status: "in_progress"} -deactivate MS - -GW --> FE: 회의 세션 정보 반환 -deactivate GW - -FE --> User: 회의 진행 화면 표시 -deactivate FE - -== 음성 녹음 시작 (비동기) == -MQ ->> STT: MeetingStarted 이벤트 전달 -activate STT -note right of STT - 구독: meeting.started - 큐: stt.meeting.started -end note - -STT -> STTDB: INSERT INTO sessions\n(세션 생성) -activate STTDB -STTDB --> STT: 세션 ID 반환 -deactivate STTDB - -STT -> STT: 음성 녹음 시작\n(실시간 스트림) -STT -->> MQ: ACK -deactivate STT - -== 실시간 음성 인식 (5초 간격 반복) == -loop 발언 발생 (5초마다) - User -> FE: 음성 발언 - activate FE - - FE -> STT: 오디오 스트림 전송 - activate STT - - STT -> Azure: 음성-텍스트 변환 요청\n(실시간 스트림) - activate Azure - Azure --> STT: 변환된 텍스트 반환\n{speaker, text, timestamp} - deactivate Azure - - STT -> STTDB: INSERT INTO transcripts\n(발언 텍스트 저장) - activate STTDB - STTDB --> STT: OK - deactivate STTDB - - STT ->> MQ: TranscriptReady 이벤트 발행\n{sessionId, speaker, text, timestamp} - activate MQ - note right of MQ - 이벤트: TranscriptReady - Routing Key: transcript.ready - 주기: 5초 - end note - MQ -->> STT: ACK - deactivate MQ - deactivate STT - - == AI 회의록 자동 작성 (비동기) == - MQ ->> AI: TranscriptReady 이벤트 전달 - activate AI - note right of AI - 구독: transcript.ready - 큐: ai.transcript.ready - end note - - AI -> AI: 회의 맥락 이해\n주제별 분류\n문장 다듬기 - AI -> LLM: 회의록 자동 작성 요청\n{context, transcript, history} - activate LLM - LLM --> AI: 회의록 초안 반환\n{summary, topics, keyPoints} - deactivate LLM - - AI -> AIDB: INSERT INTO minutes_draft\n(회의록 초안 저장) - activate AIDB - AIDB --> AI: OK - deactivate AIDB - - AI -> CS: 실시간 동기화 요청\n{delta, version} - activate CS - - CS -> FE: WebSocket 푸시\n(회의록 델타 전송) - activate FE - FE -> User: 회의록 실시간 업데이트 표시 - deactivate FE - - CS --> AI: 동기화 완료 - deactivate CS - - AI -->> MQ: ACK - deactivate AI - - == 전문용어 자동 감지 및 설명 (비동기) == - MQ ->> RAG: TranscriptReady 이벤트 전달 - activate RAG - note right of RAG - 구독: transcript.ready - 큐: rag.transcript.ready - end note - - RAG -> RAG: 전문용어 자동 감지\n(NER, 도메인 사전) - RAG -> RAGDB: SELECT explanation\nFROM terms\nWHERE term IN (detected_terms) - activate RAGDB - RAGDB --> RAG: 용어 설명 반환 - deactivate RAGDB - - alt 맥락 기반 설명 필요 - RAG -> LLM: 맥락 기반 설명 생성 요청\n{term, context, domain} - activate LLM - LLM --> RAG: 맥락적 설명 반환 - deactivate LLM - - RAG -> RAGDB: INSERT INTO explanations\n(생성된 설명 저장) - activate RAGDB - RAGDB --> RAG: OK - deactivate RAGDB - end - - RAG -> FE: WebSocket 푸시\n(용어 설명 툴팁 데이터) - activate FE - FE -> User: 용어 설명 툴팁 표시 - deactivate FE - - RAG -->> MQ: ACK - deactivate RAG -end - -@enduml diff --git a/design/backend/sequence/outer/회의예약및초대.puml b/design/backend/sequence/outer/회의예약및초대.puml deleted file mode 100644 index d9839b8..0000000 --- a/design/backend/sequence/outer/회의예약및초대.puml +++ /dev/null @@ -1,91 +0,0 @@ -@startuml 회의예약및초대 -!theme mono - -title 회의 예약 및 초대 플로우 (UFR-MEET-010) - -actor "사용자" as User -participant "Frontend" as FE -participant "API Gateway" as GW -participant "Meeting Service" as MS -participant "User Service" as US -participant "Redis" as Cache -database "Meeting DB" as MDB -queue "RabbitMQ" as MQ -participant "Notification Service" as NS - -User -> FE: 회의 정보 입력\n(제목, 날짜/시간, 장소, 참석자) -activate FE - -FE -> GW: POST /api/meetings\n(회의 예약 요청) -activate GW - -GW -> MS: 회의 생성 요청 -activate MS - -== 참석자 정보 조회 (Cache-Aside 패턴) == -loop 각 참석자 - MS -> Cache: GET user:profile:{userId} - activate Cache - - alt Cache Hit - Cache --> MS: 사용자 정보 반환 - else Cache Miss - Cache --> MS: null - MS -> US: GET /api/users/{userId}\n(사용자 프로필 조회) - activate US - US --> MS: 사용자 정보 반환 - deactivate US - - MS -> Cache: SET user:profile:{userId}\n(TTL: 10분) - Cache --> MS: OK - end - deactivate Cache -end - -== 회의 정보 저장 == -MS -> MDB: INSERT INTO meetings\n(회의 정보 저장) -activate MDB -MDB --> MS: 회의 ID 반환 -deactivate MDB - -MS -> Cache: SET meeting:info:{meetingId}\n(TTL: 10분) -activate Cache -Cache --> MS: OK -deactivate Cache - -== 초대 이벤트 발행 (비동기) == -MS ->> MQ: MeetingCreated 이벤트 발행\n{meetingId, title, attendees, datetime} -activate MQ -note right of MQ - 이벤트: MeetingCreated - Routing Key: meeting.created -end note -MQ -->> MS: ACK -deactivate MQ - -MS --> GW: 회의 ID 반환\n{meetingId, status: "scheduled"} -deactivate MS - -GW --> FE: 201 Created\n회의 생성 완료 -deactivate GW - -FE --> User: 회의 예약 완료 화면 표시 -deactivate FE - -== 초대 이메일 발송 (비동기) == -MQ ->> NS: MeetingCreated 이벤트 전달 -activate NS -note right of NS - 구독: meeting.created - 큐: notification.meeting.created -end note - -loop 각 참석자 - NS -> NS: 이메일 템플릿 생성\n(회의 제목, 일시, 장소, 초대 링크) - NS -> NS: 이메일 발송 -end - -NS -->> MQ: ACK -deactivate NS - -@enduml diff --git a/design/backend/sequence/outer/회의종료및Todo추출.puml b/design/backend/sequence/outer/회의종료및Todo추출.puml deleted file mode 100644 index 5681748..0000000 --- a/design/backend/sequence/outer/회의종료및Todo추출.puml +++ /dev/null @@ -1,194 +0,0 @@ -@startuml 회의종료및Todo추출 -!theme mono - -title 회의 종료 및 Todo 추출 플로우 (UFR-MEET-040/050, UFR-AI-020, UFR-TODO-010) - -actor "사용자" as User -participant "Frontend" as FE -participant "API Gateway" as GW -participant "Meeting Service" as MS -participant "AI Service" as AI -participant "Todo Service" as TS -participant "Notification Service" as NS -participant "Redis" as Cache -database "Meeting DB" as MDB -database "AI DB" as AIDB -database "Todo DB" as TODB -queue "RabbitMQ" as MQ -participant "LLM Server" as LLM - -== 회의 종료 == -User -> FE: 회의 종료 버튼 클릭 -activate FE - -FE -> GW: POST /api/meetings/{meetingId}/end -activate GW - -GW -> MS: 회의 종료 요청 -activate MS - -MS -> MDB: UPDATE meetings\nSET end_time = NOW(), status = 'completed'\n통계 생성 (duration, 참석자 수, 발언 수) -activate MDB -MDB --> MS: 회의 종료 시간 기록 및 통계 생성 완료 -deactivate MDB - -MS ->> MQ: MeetingEnded 이벤트 발행\n{meetingId, sessionId, endTime, statistics} -activate MQ -note right of MQ - 이벤트: MeetingEnded - Routing Key: meeting.ended -end note -MQ -->> MS: ACK -deactivate MQ - -MS --> GW: 200 OK\n{status: "completed", statistics} -deactivate MS - -GW --> FE: 회의 종료 확인 -deactivate GW - -FE --> User: 회의 종료 화면 표시 -deactivate FE - -== AI Todo 자동 추출 (비동기) == -MQ ->> AI: MeetingEnded 이벤트 전달 -activate AI -note right of AI - 구독: meeting.ended - 큐: ai.meeting.ended -end note - -AI -> AIDB: SELECT transcript, summary\nFROM minutes_draft\nWHERE session_id = {sessionId} -activate AIDB -AIDB --> AI: 회의록 전체 내용 반환 -deactivate AIDB - -AI -> AI: 액션 아이템 식별\n(동사 패턴, 시간 표현, 담당자 언급) -AI -> LLM: Todo 추출 요청\n{transcript, context, attendees} -activate LLM -LLM --> AI: Todo 목록 반환\n{tasks: [{description, assignee, dueDate, priority, linkedSection}]} -deactivate LLM - -AI -> AIDB: UPDATE minutes_draft\nSET todos_extracted = TRUE\n(Todo 추출 완료 표시) -activate AIDB -AIDB --> AI: OK -deactivate AIDB - -AI ->> MQ: TodoExtracted 이벤트 발행\n{meetingId, todos: [{description, assignee, dueDate, priority, linkedSection}]} -activate MQ -note right of MQ - 이벤트: TodoExtracted - Routing Key: todo.extracted -end note -MQ -->> AI: ACK -deactivate MQ - -AI -->> MQ: ACK (MeetingEnded) -deactivate AI - -== Todo 생성 및 할당 (비동기) == -MQ ->> TS: TodoExtracted 이벤트 전달 -activate TS -note right of TS - 구독: todo.extracted - 큐: todo.extracted -end note - -loop 각 Todo - TS -> TODB: INSERT INTO todos\n(Todo 생성, 담당자 할당, 회의록 섹션 링크) - activate TODB - TODB --> TS: Todo ID 반환 - deactivate TODB - - TS -> Cache: SET todo:list:{userId}\n(사용자별 Todo 목록 캐싱, TTL: 5분) - activate Cache - Cache --> TS: OK - deactivate Cache - - TS ->> MQ: TodoCreated 이벤트 발행\n{todoId, assignee, description, dueDate, linkedSection} - activate MQ - note right of MQ - 이벤트: TodoCreated - Routing Key: todo.created - end note - MQ -->> TS: ACK - deactivate MQ -end - -TS -->> MQ: ACK (TodoExtracted) -deactivate TS - -== Todo 할당 알림 (비동기) == -MQ ->> NS: TodoCreated 이벤트 전달 -activate NS -note right of NS - 구독: todo.created - 큐: notification.todo.created -end note - -NS -> NS: 알림 템플릿 생성\n(Todo 설명, 기한, 회의록 링크) -NS -> NS: 담당자에게 알림 발송\n(이메일, 푸시 알림) - -NS -->> MQ: ACK -deactivate NS - -== 회의 통계 및 Todo 목록 조회 == -User -> FE: 회의 상세 화면 조회 -activate FE - -FE -> GW: GET /api/meetings/{meetingId}/summary -activate GW - -GW -> MS: 회의 통계 및 Todo 목록 조회 -activate MS - -MS -> Cache: GET meeting:info:{meetingId} -activate Cache - -alt Cache Hit - Cache --> MS: 회의 정보 반환 -else Cache Miss - Cache --> MS: null - MS -> MDB: SELECT * FROM meetings\nWHERE meeting_id = {meetingId} - activate MDB - MDB --> MS: 회의 정보 반환 - deactivate MDB - - MS -> Cache: SET meeting:info:{meetingId}\n(TTL: 10분) - Cache --> MS: OK -end -deactivate Cache - -MS -> TS: GET /api/todos?meetingId={meetingId} -activate TS - -TS -> Cache: GET todo:list:meeting:{meetingId} -activate Cache - -alt Cache Hit - Cache --> TS: Todo 목록 반환 -else Cache Miss - Cache --> TS: null - TS -> TODB: SELECT * FROM todos\nWHERE meeting_id = {meetingId} - activate TODB - TODB --> TS: Todo 목록 반환 - deactivate TODB - - TS -> Cache: SET todo:list:meeting:{meetingId}\n(TTL: 5분) - Cache --> TS: OK -end -deactivate Cache - -TS --> MS: Todo 목록 반환 -deactivate TS - -MS --> GW: 회의 통계 및 Todo 목록 반환\n{statistics, todos} -deactivate MS - -GW --> FE: 200 OK -deactivate GW - -FE --> User: 회의 통계 및 Todo 목록 표시 -deactivate FE - -@enduml diff --git a/design/userstory.md b/design/userstory.md index e29685f..b9418d6 100644 --- a/design/userstory.md +++ b/design/userstory.md @@ -26,14 +26,24 @@ ## 마이크로서비스 구성 -1. **User** - 사용자 인증 및 권한 관리 -2. **Meeting** - 회의 관리, 회의록 생성 및 관리, 회의록 공유 +1. **User** - 사용자 인증 (LDAP 연동, JWT 토큰 발급/검증) +2. **Meeting** - 회의, 회의록, Todo, 실시간 협업 통합 관리 + - 회의 관리: 회의 예약, 시작, 종료 + - 회의록 관리: 회의록 생성, 수정, 확정, 공유 + - Todo 관리: Todo 할당, 진행 상황 추적, 회의록 양방향 연결 + - 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결 + - 템플릿 관리: 회의록 템플릿 관리 + - 통계 생성: 회의 및 Todo 통계 3. **STT** - 음성 녹음 관리, 음성-텍스트 변환, 화자 식별 (기본 기능) -4. **AI** - LLM 기반 회의록 자동 작성, Todo 자동 추출, 프롬프팅 기반 회의록 개선 -5. **RAG** - 맥락 기반 용어 설명, 관련 문서 검색 및 연결, 업무 이력 통합 -6. **Collaboration** - 실시간 동기화, 버전 관리, 충돌 해결 -7. **Todo** - Todo 할당 및 관리, 진행 상황 추적, 회의록 실시간 연동 -8. **Notification** - 알림 발송 및 리마인더 관리 +4. **AI** - AI 기반 회의록 자동화, Todo 추출, 지능형 검색 (RAG 통합) + - LLM 기반 회의록 자동 작성 + - Todo 자동 추출 및 담당자 식별 + - 프롬프팅 기반 회의록 개선 (1Page 요약, 핵심 요약 등) + - 관련 회의록 자동 연결 (벡터 유사도 검색) + - 전문용어 자동 감지 및 맥락 기반 설명 생성 (RAG) + - 과거 회의록 및 사내 문서 검색 + - 업무 이력 통합 +5. **Notification** - 알림 발송 및 리마인더 관리 --- @@ -55,6 +65,11 @@ AFR-USER-020: [대시보드] 사용자로서 | 나는, 회의록 서비스의 - 시나리오: 대시보드 조회 로그인 후 대시보드에 접근하면 | 예정된 회의, 진행 중 Todo, 최근 회의록 등 주요 정보가 표시되고 | 플로팅 액션 버튼을 통해 새 회의를 시작하거나 예약할 수 있다. + **변경사항 (논리 아키텍처)**: + - 프론트엔드가 Meeting Service에 직접 API 요청하여 회의, Todo, 회의록 정보 조회 + - User Service는 인증만 담당 (JWT 토큰 검증) + - 모든 API 요청에 사용자 정보(userId, userName, email) 포함 + [대시보드 주요 위젯] - 사용자 인사말 (이름 표시) - 통계 카드: @@ -110,7 +125,7 @@ AFR-USER-020: [대시보드] 사용자로서 | 나는, 회의록 서비스의 --- -2. Meeting 서비스 +2. Meeting 서비스 (회의, 회의록, Todo, 실시간 협업 통합) 1) 회의 준비 및 관리 UFR-MEET-010: [회의예약] 회의록 작성자로서 | 나는, 회의를 효율적으로 준비하기 위해 | 회의를 예약하고 참석자를 초대하고 싶다. - 시나리오: 회의 예약 및 참석자 초대 @@ -358,7 +373,7 @@ UFR-MEET-060: [회의록공유] 회의록 작성자로서 | 나는, 회의 내 --- -3. STT 서비스 (기본 기능) +3. STT 서비스 (음성 인식 및 변환 - 기본 기능) 1) 음성 인식 및 변환 UFR-STT-010: [음성녹음인식] 회의 참석자로서 | 나는, 발언 내용이 자동으로 기록되기 위해 | 음성이 실시간으로 녹음되고 인식되기를 원한다. - 시나리오: 음성 녹음 및 발언 인식 @@ -422,7 +437,7 @@ UFR-STT-020: [텍스트변환] 회의록 시스템으로서 | 나는, 인식된 --- -4. AI 서비스 (차별화 포인트) +4. AI 서비스 (AI 기반 회의록 자동화, Todo 추출, 지능형 검색 - RAG 통합) 1) AI 회의록 작성 UFR-AI-010: [회의록자동작성] 회의록 작성자로서 | 나는, 회의록 작성 부담을 줄이기 위해 | AI가 발언 내용을 자동으로 정리하여 회의록을 작성하기를 원한다. - 시나리오: AI 회의록 자동 작성 @@ -573,12 +588,18 @@ UFR-AI-040: [관련회의록연결] 회의록 작성자로서 | 나는, 이전 --- -5. RAG 서비스 (차별화 포인트) +5. AI 서비스 (지능형 검색 - RAG 기능, AI Service에 통합됨) 1) 맥락 기반 용어 설명 (강화) UFR-RAG-010: [전문용어감지] 회의록 작성자로서 | 나는, 업무 지식이 없어도 회의록을 정확히 작성하기 위해 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명을 제공받고 싶다. - 시나리오: 맥락 기반 전문용어 자동 감지 회의록이 작성되는 상황에서 | 시스템이 회의록 텍스트를 분석하면 | 전문용어가 자동으로 감지되고 맥락에 맞는 설명이 준비된다. + **변경사항 (논리 아키텍처)**: + - AI Service에 RAG 기능 통합 + - RAG와 AI 모두 LLM 기반 처리로 긴밀하게 연동 + - 동일한 벡터 임베딩 모델 및 LLM 공유 가능 + - 회의록 자동 작성 시 용어 설명이 병렬 처리되어 효율적 + [전문용어 감지 처리] - 회의록 텍스트 실시간 분석 - 용어 사전과 비교 @@ -655,12 +676,17 @@ UFR-RAG-020: [맥락기반용어설명] 회의록 작성자로서 | 나는, 전 --- -6. Collaboration 서비스 +6. Meeting 서비스 (실시간 협업 - Meeting Service에 통합됨) 1) 실시간 협업 UFR-COLLAB-010: [회의록수정동기화] 회의 참석자로서 | 나는, 회의록을 함께 검증하기 위해 | 회의록을 수정하고 실시간으로 다른 참석자와 동기화하고 싶다. - 시나리오: 회의록 실시간 수정 및 동기화 회의록 초안이 작성된 상황에서 | 참석자가 회의록 내용을 수정하면 | 수정 사항이 버전 관리되고 웹소켓을 통해 모든 참석자에게 즉시 동기화된다. + **변경사항 (논리 아키텍처)**: + - Meeting Service에 실시간 협업 기능 통합 + - WebSocket, 버전 관리, 충돌 해결이 Meeting Service 내부에서 처리됨 + - 서비스 간 통신 오버헤드 제거, 성능 향상 + [회의록 수정 처리] - 수정 내용 검증 - 수정 권한 확인 @@ -766,12 +792,17 @@ UFR-COLLAB-030: [검증완료] 회의 참석자로서 | 나는, 회의록의 정 --- -7. Todo 서비스 (차별화 포인트) +7. Meeting 서비스 (Todo 관리 - Meeting Service에 통합됨, 차별화 포인트) 1) 실시간 Todo 연결 (강화) UFR-TODO-010: [Todo할당] Todo 시스템으로서 | 나는, AI가 추출한 Todo를 담당자에게 전달하기 위해 | Todo를 실시간으로 할당하고 회의록과 연결하고 싶다. - 시나리오: Todo 실시간 할당 및 회의록 연결 AI가 Todo를 추출한 상황에서 | 시스템이 Todo를 등록하고 담당자를 지정하면 | Todo가 실시간으로 할당되고 회의록의 해당 위치와 연결되며 담당자에게 즉시 알림이 발송된다. + **변경사항 (논리 아키텍처)**: + - Meeting Service에 Todo 관리 기능 통합 + - Todo와 회의록이 동일 트랜잭션 내에서 처리 가능 + - 회의록-Todo 양방향 연결이 내부 메서드 호출로 처리됨 (10배 빠름) + [Todo 등록] - Todo 정보 저장 - Todo ID 생성 @@ -855,4 +886,97 @@ UFR-TODO-030: [Todo완료처리] Todo 담당자로서 | 나는, 완료된 Todo - M/8 +--- + +## 논리 아키텍처 반영 사항 요약 + +### 1. 마이크로서비스 구성 변경 (v2.0) + +**변경 전 (v1.0)**: 8개 마이크로서비스 +- User, Meeting, STT, AI, RAG, Collaboration, Todo, Notification + +**변경 후 (v2.0)**: 5개 마이크로서비스 +- User, Meeting, STT, AI, Notification + +### 2. 주요 변경사항 + +#### 2.1 User Service 역할 변경 +- **변경 전**: 사용자 인증 및 권한 관리, 대시보드 정보 제공 +- **변경 후**: 사용자 인증 전용 (LDAP 연동, JWT 토큰 발급/검증) +- **이유**: + - 프론트엔드가 모든 API 요청에 사용자 정보(userId, userName, email) 포함 + - User Service 동기 호출 제거 → 성능 향상, 장애 격리 + - 네트워크 지연 제거 (~100ms 개선) + +#### 2.2 Meeting Service 통합 +- **통합 서비스**: Meeting + Collaboration + Todo +- **핵심 기능**: + - 회의 관리: 회의 예약, 시작, 종료 + - 회의록 관리: 회의록 생성, 수정, 확정, 공유 + - Todo 관리: Todo 할당, 진행 상황 추적, 회의록 양방향 연결 + - 실시간 협업: WebSocket 기반 실시간 동기화, 버전 관리, 충돌 해결 +- **이점**: + - 서비스 간 통신 오버헤드 제거 + - Todo와 회의록이 동일 트랜잭션 내에서 처리 가능 + - 일관성 향상, 개발 효율성 증가 + - 내부 메서드 호출로 처리 속도 10배 향상 + +#### 2.3 AI Service 통합 +- **통합 서비스**: AI + RAG +- **핵심 기능**: + - LLM 기반 회의록 자동 작성 + - Todo 자동 추출 및 담당자 식별 + - 프롬프팅 기반 회의록 개선 + - 관련 회의록 자동 연결 (벡터 유사도 검색) + - 전문용어 자동 감지 및 맥락 기반 설명 생성 (RAG) + - 과거 회의록 및 사내 문서 검색 +- **이점**: + - RAG와 AI 모두 LLM 기반 처리로 긴밀하게 연동 + - 동일한 벡터 임베딩 모델 및 LLM 공유 가능 + - 회의록 자동 작성 시 용어 설명이 병렬 처리되어 효율적 + - 서비스 개수 감소로 운영 복잡도 감소 + +### 3. 유저스토리 영향도 + +#### 3.1 변경 없음 +- STT 서비스 유저스토리: UFR-STT-010, UFR-STT-020 +- Notification 서비스 유저스토리: 알림 발송 관련 + +#### 3.2 서비스 통합에 따른 변경 +- **Collaboration → Meeting**: UFR-COLLAB-010, UFR-COLLAB-020, UFR-COLLAB-030 +- **Todo → Meeting**: UFR-TODO-010, UFR-TODO-030 +- **RAG → AI**: UFR-RAG-010, UFR-RAG-020 + +#### 3.3 기능적 변경 +- **AFR-USER-020 (대시보드)**: + - 프론트엔드가 Meeting Service에 직접 API 요청 + - User Service는 인증만 담당 + +### 4. 성능 개선 효과 + +| 항목 | 개선 전 | 개선 후 | 효과 | +|------|---------|---------|------| +| User Service 동기 호출 | ~100ms | 제거 | 네트워크 지연 제거 | +| Todo 처리 | 서비스 간 통신 | 내부 메서드 호출 | 10배 빠름 | +| 실시간 동기화 | 서비스 간 REST API | Meeting 내부 처리 | 지연 감소 | +| 서비스 개수 | 8개 | 5개 | 운영 복잡도 감소 | + +### 5. 차별화 전략 유지 + +논리 아키텍처 변경에도 불구하고 차별화 포인트는 그대로 유지됩니다: + +- ✅ **맥락 기반 용어 설명**: AI Service에서 RAG 기능 통합 제공 +- ✅ **강화된 Todo 연결**: Meeting Service에서 더 강력한 통합 제공 +- ✅ **프롬프팅 기반 회의록 개선**: AI Service에서 계속 제공 +- ✅ **지능형 회의 진행 지원**: AI Service에서 계속 제공 + +--- + +## 문서 이력 + +| 버전 | 작성일 | 작성자 | 변경 내용 | +|------|--------|--------|----------| +| 1.0 | 2025-01-20 | 도그냥 (서비스 기획자) | 초안 작성 (8개 마이크로서비스) | +| 2.0 | 2025-01-22 | 길동 (아키텍트) | 논리 아키텍처 반영 (5개 마이크로서비스로 단순화) | + --- \ No newline at end of file