MalJosim은 RAG(Retrieval-Augmented Generation)와 LLM을 활용한 욕설 필터링 API 서비스입니다.
단순 키워드 매칭을 넘어서 컨텍스트를 이해하고 더 정확한 필터링을 제공하는 것이 목표입니다.
word: 원본 금칙어normalizedWord: 정규화된 형태 (자모 합치기, leetspeak 변환 등)severity: 심각도 레벨 (LOW, MEDIUM, HIGH, CRITICAL)category: 카테고리 (PROFANITY, HATE_SPEECH, SEXUAL, VIOLENCE, SPAM, OTHER)isActive: 활성화 여부 (소프트 삭제)aliases: 별칭/변형 리스트overrideSeverity를 통한 클라이언트별 심각도 재정의generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum Severity {
LOW
MEDIUM
HIGH
CRITICAL
}
enum Category {
PROFANITY
HATE_SPEECH
SEXUAL
VIOLENCE
SPAM
OTHER
}
enum HistoryAction {
CREATE
UPDATE
DELETE
}
model BadWord {
id String @id @default(uuid())
word String @db.VarChar(100)
normalizedWord String @db.VarChar(100)
severity Severity @default(MEDIUM)
category Category @default(OTHER)
isActive Boolean @default(true)
aliases String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
clientBadWords ClientBadWord[]
// Indexes
@@index([normalizedWord])
@@index([isActive])
@@index([category])
@@unique([word])
}
model ClientBadWord {
clientId String @db.VarChar(100)
wordId String
overrideSeverity Severity?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
badWord BadWord @relation(fields: [wordId], references: [id], onDelete: Cascade)
// Composite Primary Key
@@id([clientId, wordId])
@@index([clientId])
@@index([wordId])
}
model History {
id String @id @default(uuid())
tableName String @db.VarChar(50)
recordId String
action HistoryAction
userId String? @db.VarChar(100)
createdAt DateTime @default(now())
@@index([tableName, recordId])
@@index([createdAt])
}
POST /bad-words: 금칙어 생성GET /bad-words: 금칙어 목록 조회 (페이지네이션, 필터링 지원)GET /bad-words/:id: 특정 금칙어 조회PATCH /bad-words/:id: 금칙어 수정DELETE /bad-words/:id: 금칙어 삭제 (소프트 삭제)word 필드에 unique 제약 조건으로 중복 방지CreateBadWordDto: 생성용 DTOUpdateBadWordDto: 수정용 DTO (부분 업데이트 지원)QueryBadWordDto: 조회용 쿼리 파라미터 DTOBadWordResponseDto: 응답용 DTObad_words:global - 모든 활성 금칙어의 normalizedWord 저장bad_words:normalized:{normalizedWord} - 정규화된 단어 → 원본 단어 정보bad_words:detail:{id} - 단어의 전체 정보bad_words:client:{clientId} - 클라이언트별 활성 금칙어onModuleInit) PostgreSQL에서 활성 금칙어를 로드하여 Redis에 저장loadGlobalBadWords(): 글로벌 금칙어 로드loadClientBadWords(clientId): 클라이언트별 금칙어 로드isBadWord(normalizedWord): 글로벌 금칙어 여부 확인isClientBadWord(clientId, normalizedWord): 클라이언트별 금칙어 여부 확인addBadWord(): 금칙어 추가 (Write-through)updateBadWord(): 금칙어 업데이트 (Write-through)removeBadWord(): 금칙어 삭제 (Write-through)서비스의 상태를 모니터링하기 위한 헬스 체크 엔드포인트 구현:
src/
├── app.module.ts # 루트 모듈
├── bad-word/ # 금칙어 관리 모듈
│ ├── bad-word.controller.ts
│ ├── bad-word.service.ts
│ ├── bad-word.module.ts
│ └── dto/ # Data Transfer Objects
├── cache/ # 캐시 모듈
│ ├── cache.service.ts
│ ├── cache.module.ts
│ └── cache-keys.ts # Redis 키 관리
├── database/ # 데이터베이스 모듈
│ ├── prisma.service.ts
│ └── database.module.ts
└── health/ # 헬스 체크 모듈
├── health.controller.ts
└── health.service.ts
buildCreateData(), buildUpdateData())NotFoundException: 리소스를 찾을 수 없을 때ConflictException: 중복 데이터 생성 시도 시normalizedWord 인덱스: 정규화된 단어 기준 빠른 조회isActive 인덱스: 활성 금칙어만 필터링category 인덱스: 카테고리별 통계/정책 조회word unique 제약: 중복 방지 및 빠른 조회loadGlobalBadWords()에서 대량 데이터 로드 시 활용findAll()에서 Promise.all()을 사용하여 count와 데이터 조회를 병렬 처리필터링 엔진 구현
RAG + LLM 통합
클라이언트별 정책 API
테스트 코드 작성
API 문서화
배포 및 인프라
isActive 플래그를 통한 소프트 삭제 구현현재까지 기본적인 CRUD API와 캐싱 인프라를 구축했습니다.
다음 단계로 실제 필터링 엔진과 AI 모델 연동을 진행할 예정입니다.
프로젝트 저장소: [GitHub 링크]
기술 문서: docs/rdb-schema.md 참고