generated from ttipo/project-base
cd958f3bf4
직원이 웹메일 · 휴대폰 앱에서 무엇이 새로 생겼는지 읽는 게시판의 서버 몫이다. 글은 운영자가 backend/release-notes/ 에 글마다 파일 하나로 쓰고
(올리는 API · DB 표 · 새 에러 코드 없음), GET /api/release-notes(커서 목록)와 GET /api/release-notes/{id}(글 + 바로 옛 글 · 새 글)로 웹 · 앱에 똑같이 준다.
- 글 폴더는 src 밖이라 개발 컨테이너(바인드 마운트)는 다시 띄우지 않아도 새 글이 보이고, 운영 이미지는 Dockerfile 이 폴더를 함께 복사한다
- 틀린 글은 실행 때 그 글만 건너뛰고 error 로그(파일 이름 · 줄 · 까닭만 — 기동 때 한 번 읽어 배포 직후 보인다), 실제 폴더를 엄격하게 읽는 시험이 CI 에서 운영자의 오타를 잡는다
- 파일은 O_NOFOLLOW 로 한 번 열어 일반 파일 · 256KB 까지만 읽고, 모양이 틀린 id 도 없는 글과 같은 404 RES_001 이다
- 첫 글(2026-11-18.md)은 다음 배포 소식이다 — 파일 이름(날짜)은 배포 날로 고친다
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.8 KiB
TypeScript
115 lines
5.8 KiB
TypeScript
import { Module } from '@nestjs/common';
|
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
import { ConfigModule } from '@nestjs/config';
|
|
import { ScheduleModule } from '@nestjs/schedule';
|
|
import { ThrottlerModule } from '@nestjs/throttler';
|
|
import { CacheModule } from '@nestjs/cache-manager';
|
|
import { APP_GUARD } from '@nestjs/core';
|
|
|
|
import { ClientIpThrottlerGuard } from '@/common/guards/client-ip-throttler.guard';
|
|
import { OriginGuard } from '@/common/guards/origin.guard';
|
|
import { AccountSecurityModule } from '@/modules/account-security/account-security.module';
|
|
import { AppConfigModule } from '@/modules/app-config/app-config.module';
|
|
import { AppReleasesModule } from '@/modules/app-releases/app-releases.module';
|
|
import { AuthModule } from '@/modules/auth/auth.module';
|
|
import { ContactsModule } from '@/modules/contacts/contacts.module';
|
|
import { FileSharingModule } from '@/modules/file-sharing/file-sharing.module';
|
|
import { FileStorageModule } from '@/modules/file-storage/file-storage.module';
|
|
import { HealthModule } from '@/modules/health/health.module';
|
|
import { MailModule } from '@/modules/mail/mail.module';
|
|
import { PushModule } from '@/modules/push/push.module';
|
|
import { ReleaseNotesModule } from '@/modules/release-notes/release-notes.module';
|
|
import { SystemSettingsModule } from '@/modules/system-settings/system-settings.module';
|
|
import { UploadModule } from '@/modules/upload/upload.module';
|
|
import { UserModule } from '@/modules/user/user.module';
|
|
import { parsePositiveInt } from '@/shared/utils/env';
|
|
import { buildDatabaseOptions } from '@/shared/config/database.config';
|
|
import { validateEnv } from '@/shared/config/env.validation';
|
|
import { ShutdownLogger } from '@/shared/logger/shutdown.logger';
|
|
|
|
// Rate Limiting 기본 정책 — 루트 .env 의 THROTTLE_* 로 덮어쓸 수 있다 (.claude/rules/env-structure.md)
|
|
const THROTTLE_DEFAULT_TTL_MS = 60_000; // 집계 구간: 60초
|
|
const THROTTLE_DEFAULT_LIMIT = 100; // 구간당 IP별 최대 요청 수
|
|
|
|
@Module({
|
|
imports: [
|
|
ScheduleModule.forRoot(),
|
|
ConfigModule.forRoot({
|
|
isGlobal: true,
|
|
envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
|
|
// 설정이 잘못되면 조용히 기본값으로 굴러가지 않고 기동 시점에 실패시킨다
|
|
validate: validateEnv,
|
|
}),
|
|
// 전역 캐시 매니저 — Redis 도입 시 store 옵션을 redisStore 로 변경
|
|
CacheModule.register({
|
|
isGlobal: true,
|
|
ttl: 60_000, // 60초 기본 TTL
|
|
}),
|
|
// 전역 Rate Limiter — API 남용/브루트포스 방지 (프로젝트의 유일한 rate limiting 수단)
|
|
// 한도 초과 시 429 → 전역 필터가 REQ_001 로 매핑한다.
|
|
// 라우트 단위 조정은 @Throttle({ default: { ttl, limit } }), 제외는 @SkipThrottle().
|
|
ThrottlerModule.forRoot([
|
|
{
|
|
// compose 가 미정의 변수를 빈 문자열로 주입하므로 parsePositiveInt 로 방어한다
|
|
ttl: parsePositiveInt(
|
|
process.env.THROTTLE_TTL_MS,
|
|
THROTTLE_DEFAULT_TTL_MS,
|
|
),
|
|
limit: parsePositiveInt(
|
|
process.env.THROTTLE_LIMIT,
|
|
THROTTLE_DEFAULT_LIMIT,
|
|
),
|
|
},
|
|
]),
|
|
// DB 접속 설정은 database.config.ts 하나만 수정한다 (CLI 와 공유)
|
|
TypeOrmModule.forRootAsync({
|
|
useFactory: () => ({
|
|
...buildDatabaseOptions(),
|
|
// Nest 모듈에 등록된 엔티티를 자동으로 수집한다 (CLI 는 glob 경로를 사용)
|
|
autoLoadEntities: true,
|
|
}),
|
|
}),
|
|
HealthModule,
|
|
// 직원 로그인(쿠키 JWT) — 자격 확인은 MailModule 의 메일 엔진 포트로
|
|
AuthModule,
|
|
// 계정 보안 (12단계) — 2단계 인증 · 앱 비밀번호 · 로그인 기록 · 엔진 웹훅 받기 (인증 모듈도 가져온다)
|
|
AccountSecurityModule,
|
|
// 웹메일 API (Stalwart JMAP 어댑터)
|
|
MailModule,
|
|
// 주소록 API — 내 주소록 · 사내 목록 (8단계, 메일 모듈도 받는 사람 추천에 쓴다)
|
|
ContactsModule,
|
|
// 대용량 첨부 (11단계) — 파일 · 조각 올리기 · 악성 검사(FileStorageModule)와 공개 링크 · 내려받기(FileSharingModule).
|
|
// 메일 모듈도 가져온다(메일 → 공유 → 저장 한 방향) — 2부 파일 저장소가 같은 두 모듈을 쓴다
|
|
FileStorageModule,
|
|
FileSharingModule,
|
|
// 서버 설정 (13단계) — 2단계 인증 필수 정책 · 문의 연락처(GET /api/support-contact). 운영 도구가 바꾼다
|
|
SystemSettingsModule,
|
|
// 앱 알림 (앱 1단계) — 등록(/api/push-registrations/current) · 끝난 세션 정리 · 보내기(FCM). 새 메일을 알아채는 쪽은 메일 모듈이다
|
|
PushModule,
|
|
// 앱 설정 (앱 1단계) — GET /api/app-config (로그인 없음 — 최소 판 · 알림을 보낼 수 있는지)
|
|
AppConfigModule,
|
|
// 휴대폰 앱 받기 (사내 배포) — 최신 판 정보 · APK 파일 (로그인 필요). 올리기는 운영 도구다
|
|
AppReleasesModule,
|
|
// 업데이트 내역 — 목록 · 글 (로그인 필요). 글은 저장소의 backend/release-notes 폴더(글 파일 하나가 글 하나), 올리는 API 는 없다
|
|
ReleaseNotesModule,
|
|
UploadModule,
|
|
UserModule,
|
|
],
|
|
providers: [
|
|
ShutdownLogger,
|
|
// 전역 가드는 등록 순서대로 돈다 — 다른 사이트에서 온 상태 변경 요청은 요청 수를 세기 전에 막는다
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: OriginGuard,
|
|
},
|
|
// 요청 수 한도 — ThrottlerGuard 그대로, 클라이언트 키만 IPv6 /64 로 묶는다 (두 번째 limiter 가 아니다)
|
|
{
|
|
provide: APP_GUARD,
|
|
useClass: ClientIpThrottlerGuard,
|
|
},
|
|
],
|
|
})
|
|
// 요청 추적 ID 는 app.setup.ts 의 configureApp() 이 **맨 앞** express 미들웨어로 붙인다 —
|
|
// 본문 파서 오류(413·400·415)에도 traceId 가 남도록 Nest 미들웨어(본문 파서 뒤)로 두지 않는다
|
|
export class AppModule {}
|