Files
comrelay/backend/src/app.module.ts
T
ttipo bf2321f2d5 feat: 화상회의 단계 1 — LiveKit 기반 인프라 토대
LiveKit(SFU)에 미디어 전송을 위임하고, 앱은 입장 토큰 발급 + 회의 화면만
담당하는 구조로 단계 1(기본 연결)을 구현한다.

백엔드
- meeting 모듈: POST /api/meetings/token (JWT 인증)
  livekit-server-sdk 로 입장 JWT 발급(identity=userId, name, grants:
  roomJoin/publish/subscribe/publishData, TTL 1h). 단계 2에서 역할별 분기 예정.

프론트
- useMeeting(토큰 발급), useLiveKitRoom(연결/참여자/컨트롤 상태)
- ParticipantTile(카메라/마이크 트랙 attach, 본인 거울·음소거, 이니셜 플레이스홀더)
- MeetingPage(로비 + 영상 그리드), 라우트 /meeting/:room?, 사이드바 네비 추가
- VITE_LIVEKIT_URL 주입(build arg + dev 런타임 env)

인프라/설정
- 개발 미디어 서버는 LiveKit Cloud 사용(로컬 self-host 는 WSL2/Docker Desktop
  WebRTC 네트워킹 제약으로 제외). env 만으로 운영 전환 가능.
- livekit/livekit.yaml 은 단계 6 셀프호스팅 참고용으로 보관.
- 화상회의_화면명세.md: 현재 화면 기능/구조/데이터 계약 문서(UI 개편 참고).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:40:18 +09:00

116 lines
4.7 KiB
TypeScript

import { Logger, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule as CronModule } from '@nestjs/schedule';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { CacheModule } from '@nestjs/cache-manager';
import { createKeyv } from '@keyv/redis';
import type Keyv from 'keyv';
import { APP_GUARD } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HealthModule } from './modules/health/health.module';
import { UserModule } from './modules/user/user.module';
import { AuthModule } from './modules/auth/auth.module';
import { TaskModule } from './modules/task/task.module';
import { ActivityModule } from './modules/activity/activity.module';
import { NotificationModule } from './modules/notification/notification.module';
import { AiModule } from './modules/ai/ai.module';
import { ProjectModule } from './modules/project/project.module';
import { ScheduleModule } from './modules/schedule/schedule.module';
import { MeetingModule } from './modules/meeting/meeting.module';
@Module({
imports: [
CronModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
}),
// 전역 캐시 매니저 — REDIS_* 설정 시 Redis(keyv) 스토어, 없으면 인메모리 폴백.
// docker-compose 가 REDIS_HOST/PORT/PASSWORD 를 주입한다(단독 실행/테스트는 미설정 → 인메모리).
CacheModule.registerAsync({
isGlobal: true,
useFactory: (): { ttl: number; stores?: Keyv[] } => {
const logger = new Logger('CacheModule');
const options: { ttl: number; stores?: Keyv[] } = { ttl: 60_000 };
const host = process.env.REDIS_HOST;
if (!host) {
// Redis 미설정 — 인메모리 폴백(LRU). 로컬/테스트에서 Redis 없이도 동작.
// 주의: 다중 인스턴스를 인메모리 모드로 띄우면 인스턴스별 캐시·무효화가 분리됨(단일 인스턴스 전용)
logger.log('캐시: 인메모리 모드(단일 인스턴스) — REDIS_HOST 미설정');
return options;
}
const port = process.env.REDIS_PORT || '6379';
const password = process.env.REDIS_PASSWORD;
const auth = password ? `:${encodeURIComponent(password)}@` : '';
const keyv = createKeyv(`redis://${auth}${host}:${port}`);
// Redis 연결 오류가 unhandled 로 프로세스를 죽이지 않도록 흡수(캐시 장애는 DB 조회로 폴백)
keyv.on('error', (err: unknown) => {
logger.warn(
`Redis 캐시 오류(무시, DB 폴백): ${err instanceof Error ? err.message : String(err)}`,
);
});
options.stores = [keyv];
logger.log('캐시: Redis 모드(다중 인스턴스 무효화 정합)');
return options;
},
}),
// 전역 Rate Limiter — main.ts 의 express-rate-limit 와 별개로 라우트 단위 제어 가능
// NOTE: 개발/테스트 편의를 위해 분당 1000회로 완화함. 운영 배포 전 적정값으로 되돌릴 것.
ThrottlerModule.forRoot([
{
ttl: 60_000,
limit: 1000,
},
]),
TypeOrmModule.forRootAsync({
useFactory: () => {
const isProduction = process.env.NODE_ENV === 'production';
return {
type: 'postgres',
host: process.env.DB_HOST || 'db',
port: parseInt(process.env.DB_PORT || '5432', 10),
username:
process.env.DB_USER || process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database:
process.env.DB_NAME || process.env.DB_DATABASE || 'project_db',
autoLoadEntities: true,
// 운영 환경에서는 절대 synchronize 사용 금지 (스키마 자동 변경 → 데이터 손실 위험)
synchronize: !isProduction,
migrationsRun: isProduction,
logging: !isProduction,
extra: {
max: 100,
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
},
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
};
},
}),
HealthModule,
UserModule,
AuthModule,
TaskModule,
ActivityModule,
NotificationModule,
AiModule,
ProjectModule,
ScheduleModule,
MeetingModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}