You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
이 문서는 아키텍처 패턴과 도메인 모델(DDD) 을, 실제 구현된 코드 기준으로 정의한다.
"왜 이렇게 설계했나"와 "코드의 어디에 무엇이 있나"를 함께 담는다. 코드를 따라가며
읽는 실무 가이드는 CODEBASE_GUIDE.md, 범위·의사결정은
PLAN.md를 참고한다.
Backend — Bounded Context 별 NestJS 모듈. 각 모듈은 interface/ application/ domain/ infrastructure/
4계층. 의존 방향은 항상 안쪽(Domain)을 향하고, Domain은 프레임워크를 import 하지 않는다.
Application은 Domain에 정의된 Port(인터페이스) 로만 Infrastructure와 통신하며, 구현체는
NestJS DI(주로 useFactory)로 주입된다.
Frontend — View는 props만 받는 dumb 컴포넌트로 fetch/useState/useEffect/socket/
zustand setter를 직접 호출하지 않는다. 모든 상태·부수효과는 ViewModel hook에, 데이터 접근은
Model(zustand store + api/socket 모듈)에 둔다.
2. 도메인 모델 (DDD)
2.1 Ubiquitous Language
용어
정의
Meeting
한 번의 회의 세션. code로 입장. hard limit 없음, 전원 퇴장 후 idle 지속 시 자동 종료. host 토큰 보유자만 수동 종료.
Participant
닉네임만으로 입장한 사용자(회원 아님). socket id가 식별자.
MeetingCode
8자 소문자 영숫자 입장 식별자(VO). URL /meetings/[code].
ParticipantMedia
한 참가자의 미디어 상태(transport/producer/consumer 식별자). Mediasoup BC의 Aggregate.
MeetingReport
회의 종료 시 만들어지는 회의록 Aggregate(제목·요약·결정·액션·토픽·transcript·chat). MongoDB 영속.
컨텍스트 간 결합은 Domain Event(@nestjs/event-emitter) 또는 Port로만. Aggregate·Service·
Repository를 BC 간 직접 import 하지 않는다(CLAUDE.md hard rule 7).
여러 BC가 공유하는 read-only VO·이벤트 payload는 shared-kernel/domain/에 둔다(DDD Shared Kernel).
전 BC 공통 에러 계층(framework-free DomainError + 의미 기반 NotFoundError/ForbiddenError/ConflictError)은
shared-kernel/domain/errors.ts, 이를 HTTP로 번역하는 글로벌 DomainExceptionFilter는 shared-kernel/interface/에 둔다.
chat은 별도 모듈이 아니라 Meeting BC 안에서 처리된다(gateway meeting:chat → MeetingService.postChat
→ Redis ChatRepository). 회의 종료 시 누적 chat이 meeting.ended payload로 Reports에 이관된다.
2.3 Aggregate / Entity / VO
Aggregate Root
위치
Entity / VO
핵심 불변식
Meeting
meeting/domain/meeting.ts
Entity Participant; VO MeetingCode·IdleTimeout; 공유 VO Source·ExternalReference
endedAt이 채워지면 mutating 거부; idle은 활성 0명일 때만 작동; host 토큰 일치자만 수동 종료
ParticipantMedia
mediasoup/domain/participant-media.ts
producer/consumer entry; MediaType
send transport 없이 producer 불가; producerId/consumerId 중복 불가
MeetingReport
reports/domain/meeting-report.ts
TranscriptSegment·ParticipantEntry(entries/); VO ReportSummary·ActionItem·KeyTopic·PipelineState·NotionPushResult
summaryStatus==='done' 일 때만 summary 존재; pipeline이 final 이어야 Notion push
2.4 Domain Event 카탈로그
이벤트 이름 문자열의 단일 진실원은 packages/shared-interfaces/src/events.ts.
SystemClock·NestEventBusDomainEventPublisher(@Global SharedKernelModule), 공유 VO(Source·ExternalReference·ChatEntry), cross-BC 이벤트 payload(MeetingEndedPayload 등), 에러 계층(DomainError)과 글로벌 DomainExceptionFilter(AppModule APP_FILTER)
redis/ · mongo/
인프라 모듈(@Global)
ioredis 클라이언트, mongoose connection
config/
부트스트랩 env 해석
server·mongo·redis·gemini·ai-worker·mediasoup·admin config 순수 함수
전역 설정: ConfigModule.forRoot({ isGlobal: true }), EventEmitterModule.forRoot({ wildcard: true }),
글로벌 ValidationPipe({ whitelist, forbidNonWhitelisted, transform })(main.ts), WS는 게이트웨이의
@UsePipes로 동일 ValidationPipe + WsException exceptionFactory 적용. 글로벌 DomainExceptionFilter(APP_FILTER)가
DomainError의 httpStatus만 읽어 HTTP 응답으로 매핑하므로, 컨트롤러는 도메인 에러를 try/catch 하지 않는다.
View 규칙(강제): View는 useState/useEffect/fetch/socket/zustand setter를 직접 호출하지
않는다. 필요한 데이터·콜백은 ViewModel hook 반환에서 받는다. ViewModel 반환은 인터페이스로
명시(UseMeetingViewModel)해 View가 그대로 prop 타입으로 받게 한다(테스트 용이).
정적 export 제약: output: 'export'. server component 데이터 fetch·route.ts·middleware 금지.
동적 라우트는 placeholder HTML 한 장만 빌드되므로 useParams()가 빌드 타임 값을 돌려준다 → useRouteSegment가
window.location.pathname에서 실제 코드/id를 읽는다(useParams 폴백). 데이터는 fetch(NEXT_PUBLIC_API_URL/...).
CloudFront /404 → /index.html fallback.
STT/Summary 실패도 도큐먼트는 영속되며 pipeline.failures[]에 기록된다(재시도는 v2).
회의 중에는 PartialTranscriptionScheduler가 30초마다 부분 STT를 누적해, 종료 시 잔여만 처리한다.
6. 핵심 시퀀스 (회의 한 건)
sequenceDiagram
participant U as User(Browser)
participant VM as Frontend(ViewModel)
participant GW as Gateway/Controller
participant SVC as App Service
participant BUS as Event Bus
participant R as Redis
participant W as ai-worker(STT)
participant L as Gemini
participant M as Mongo
U->>VM: 회의 생성(제목/닉네임)
VM->>GW: POST /meetings
GW->>SVC: createMeeting
SVC->>R: save(Meeting)
SVC-->>VM: { code, hostToken }
U->>VM: 입장(code)
VM->>GW: WS meeting:join + mediasoup:* 시그널링
GW->>SVC: joinMeeting / produce·consume
SVC->>BUS: participant.joined / producer.created
Note over U,VM: 회의 진행 — 채팅·미디어, 오디오는 ffmpeg→Redis 버퍼, 30s 부분 STT
U-->>GW: host 종료 또는 전원 퇴장 후 idle
SVC->>BUS: meeting.ended
BUS->>SVC: Reports.createDraft (Mongo draft)
SVC->>BUS: report.transcription.requested
BUS->>W: transcribe(잔여 audio)
W-->>SVC: segments → transcription.completed
SVC->>L: summarize(transcript + chat)
L-->>SVC: ReportSummary → finalize
SVC->>M: save(MeetingReport)
U->>GW: GET /reports/:id
GW-->>U: 회의록
Loading
7. v2 확장 지점 (변경 없이 추가)
새 BC notion/ 추가 → report.finalized/report.summary.completed 구독.