μ¬μ©μκ° μμ±ν κ²μκΈμ μ΄μμ μ±
λ£° κΈ°λ°μΌλ‘ μλ κ²μνκ³ , μ€νΈ/μ¬κΈ°/μ΄λ·°μ§ μνλλ₯Ό κ³μ°ν΄ ALLOW, REVIEW, BLOCK μ€ νλλ‘ νμ ν©λλ€.
| ALLOW | BLOCK | REVIEW |
|---|---|---|
![]() |
![]() |
![]() |
μ΄μνμ λ§€μΌ λ§μ κ²μκΈμ κ²μν΄μΌ νκ³ , λ°λ³΅μ μΈ μ μ± μλ° ν€μλ νμ§μ μνλ νλ¨μ μμμ μΌλ‘ μ²λ¦¬νλ©΄ μλμ μΌκ΄μ±μ΄ λ¨μ΄μ§λλ€.
νΉν λ€μκ³Ό κ°μ ν¨ν΄μ λΉ λ₯΄κ² κ°μ§ν΄ μ΄μμκ° μ°μ μμλ₯Ό μ‘μ μ μμ΄μΌ ν©λλ€.
- μ μ κΈ μꡬ
- μΈλΆ μ°λ½ μ λ
- κ³μ’/μ‘κΈ μ λ
- λΆλ² κ±°λ ν€μλ
- μ€νΈμ± νν
μμμ κ²μλ§μΌλ‘ μ²λ¦¬ν κ²½μ° λ€μ λ¬Έμ κ° λ°μν μ μμ΅λλ€.
| λ¬Έμ | μ€λͺ |
|---|---|
| μ²λ¦¬ μ§μ° | κ²μκΈμ΄ λ§μμ§μλ‘ κ²μ λκΈ° μκ°μ΄ μ¦κ° |
| νλ¨ μΌκ΄μ± λΆμ‘± | μ΄μμλ§λ€ μ μ± μλ° νλ¨ κΈ°μ€μ΄ λ¬λΌμ§ μ μμ |
| λ°λ³΅ μ 무 μ¦κ° | λ¨μ ν€μλ κ²μμλ μ΄μ 리μμ€κ° μλͺ¨λ¨ |
| μ¬ν λμ κ°λ₯μ± | μν μ½ν μΈ κ° μ¬μ©μμκ² λ ΈμΆλ λ€ μ²λ¦¬λ μ μμ |
μν ν€μλκ° ν¬ν¨λ ν μ€νΈ κ²μκΈ μμ μΊ‘μ²λ₯Ό λ£λ μμμ λλ€.
λ³Έ νλ‘μ νΈμμλ μ΄μμ μ± λ£°μ μ½λμ νλμ½λ©νμ§ μκ³ DBμ μ μ₯ν λ€, νμ±νλ λ£°λ§ κ²μμ μ¬μ©νλλ‘ μ€κ³νμ΅λλ€.
- μ΄μμ μ± λ£°μ PostgreSQLμ μ μ₯
- μ λͺ©κ³Ό λ³Έλ¬Έμμ ν€μλ λ§€μΉ
- λ§€μΉλ λ£°μ μ μλ₯Ό ν©μ°ν΄ μνλ κ³μ°
- μ μ κΈ°μ€μ λ°λΌ
ALLOW,REVIEW,BLOCKνμ - κ²μ κ²°κ³Όμ λ§€μΉλ λ£°μ PostgreSQLμ λ‘κ·Έλ‘ μ μ₯
- λμΌ μ½ν μΈ μ¬κ²μλ Redis μΊμλ‘ μλ΅
νμ κΈ°μ€μ λ€μκ³Ό κ°μ΅λλ€.
| μ μ λ²μ | νμ | μλ―Έ |
|---|---|---|
| 0 ~ 39 | ALLOW | μ μ κ²μκΈ |
| 40 ~ 79 | REVIEW | μ΄μμ κ²ν νμ |
| 80 μ΄μ | BLOCK | μ°¨λ¨ λμ |
| κ΅¬λΆ | κΈ°μ |
|---|---|
| Language | Python 3.11+ |
| Framework | FastAPI |
| Database | PostgreSQL |
| ORM | SQLAlchemy |
| Migration | Alembic |
| Validation | Pydantic |
| Cache | Redis |
| Infra | Docker Compose |
| Test | pytest |
| API Docs | FastAPI Swagger UI |
flowchart LR
Client[Client / μ΄μμ] --> API[FastAPI Server]
API --> Redis[(Redis)]
API --> DB[(PostgreSQL)]
DB --> Rules[moderation_rules]
DB --> Logs[moderation_logs]
API --> Hash[Content Hash μμ±]
Hash --> Redis
API --> RuleEngine[Rule Engine]
RuleEngine --> Decision[ALLOW / REVIEW / BLOCK]
Decision --> Logs
GitHub READMEμ Mermaid λ λλ§ νλ©΄ λλ mermaid.liveμμ PNGλ‘ λ½μ μ΄λ―Έμ§λ₯Ό λ£λ μμμ λλ€.
- ν΄λΌμ΄μΈνΈκ°
POST /api/moderations/checkλ‘ μ½ν μΈ λ₯Ό μ μ‘ν©λλ€. - μλ²κ°
title + content + price + categoryλ‘contentHashλ₯Ό μμ±ν©λλ€. - Redisμμ
moderation:content:{contentHash}keyλ₯Ό μ‘°νν©λλ€. - cache hitμ΄λ©΄ λ£° κ³μ°κ³Ό DB μ‘°ν μμ΄ μ¦μ μλ΅ν©λλ€.
- cache missμ΄λ©΄ νμ±νλ λ£°μ DBμμ μ‘°νν©λλ€.
- μ λͺ©κ³Ό λ³Έλ¬Έμμ ν€μλλ₯Ό λ§€μΉν©λλ€.
- λ§€μΉλ λ£°μ μ μλ₯Ό ν©μ°ν΄ μν μ μλ₯Ό κ³μ°ν©λλ€.
- μν μ μ κΈ°μ€μΌλ‘
ALLOW,REVIEW,BLOCKμ νμ ν©λλ€. - κ²μ λ‘κ·Έλ₯Ό PostgreSQLμ μ μ₯ν©λλ€.
- κ²μ κ²°κ³Όλ₯Ό Redisμ 5λΆ TTLλ‘ μ μ₯ν©λλ€.
- ν΄λΌμ΄μΈνΈμκ² νμ κ²°κ³Όλ₯Ό λ°νν©λλ€.
sequenceDiagram
participant Client as Client
participant API as FastAPI Server
participant Redis as Redis
participant DB as PostgreSQL
Client->>API: POST /api/moderations/check
API->>API: title + content + price + categoryλ‘ contentHash μμ±
API->>Redis: moderation:content:{contentHash} μ‘°ν
alt Cache Hit
Redis-->>API: μΊμλ κ²μ κ²°κ³Ό λ°ν
API-->>Client: decision, riskScore, matchedRules λ°ν
else Cache Miss
API->>DB: enabled=true μ΄μμ μ±
λ£° μ‘°ν
DB-->>API: νμ±νλ rules λ°ν
API->>API: ν€μλ λ§€μΉ
API->>API: riskScore κ³μ°
API->>API: ALLOW / REVIEW / BLOCK νμ
API->>DB: moderation_logs μ μ₯
API->>Redis: κ²μ κ²°κ³Ό μΊμ±, TTL 5λΆ
API-->>Client: decision, riskScore, matchedRules λ°ν
end
ν¬νΈν΄λ¦¬μ€ λ¬Έμμ λ£κΈ° μ’μ ν΅μ¬ μΊ‘μ²μ λλ€.
erDiagram
MODERATION_RULES {
int id PK
string ruleName
string keyword
int score
string action
string category
boolean enabled
datetime createdAt
datetime updatedAt
}
MODERATION_LOGS {
int id PK
int userId
string title
string contentHash
int riskScore
string decision
string matchedRules
string reason
datetime createdAt
}
μ΄μμ μ± λ£°μ μ μ₯ν©λλ€.
| νλ | μ€λͺ |
|---|---|
| id | λ£° ID |
| ruleName | λ£° μ΄λ¦ |
| keyword | κ°μ§ ν€μλ |
| score | μν μ μ |
| action | κΈ°λ³Έ μ‘μ ννΈ |
| category | μ μ± μΉ΄ν κ³ λ¦¬ |
| enabled | λ£° νμ±ν μ¬λΆ |
| createdAt | μμ± μκ° |
| updatedAt | μμ μκ° |
κ²μ μμ²μ νμ κ²°κ³Όλ₯Ό μ μ₯ν©λλ€.
| νλ | μ€λͺ |
|---|---|
| id | λ‘κ·Έ ID |
| userId | μμ±μ ID |
| title | κ²μκΈ μ λͺ© |
| contentHash | μ½ν μΈ ν΄μ |
| riskScore | μν μ μ |
| decision | μ΅μ’ νμ |
| matchedRules | λ§€μΉλ λ£° μ΄λ¦ λͺ©λ‘ |
| reason | νμ μ¬μ |
| createdAt | μμ± μκ° |
FastAPI μλ λ¬Έμλ μ€ν ν μλ μ£Όμμμ νμΈν μ μμ΅λλ€.
κ°λ¨ν μ΄μ νμΈ UIλ μ€ν ν http://localhost:8000/μμ μ¬μ©ν μ μμ΅λλ€.
UIμ λΆνν
μ€νΈ / κ΄μΈ‘ μμμμ μμ² μ, λμμ±, cached/unique λͺ¨λλ₯Ό μ νν΄ λΆνν
μ€νΈλ₯Ό μ€νν μ μμ΅λλ€. μ€ν κ²°κ³Όλ‘ RPS, νκ· μ§μ°, p95 μ§μ°, μλ¬ μ, RSS λ©λͺ¨λ¦¬, DB 컀λ₯μ
ν checked-out/overflow, Redis ping, λ³λͺ© ννΈλ₯Ό νμΈν μ μμ΅λλ€.
http://localhost:8000/docs
POST /api/moderations/check{
"userId": 1,
"title": "μμ΄ν° μΈκ² νλλ€",
"content": "μ μ
κΈνλ©΄ νλ°° 보λ΄λλ €μ. μΉ΄ν‘ μ£ΌμΈμ.",
"price": 100000,
"category": "DIGITAL"
}{
"decision": "REVIEW",
"riskScore": 70,
"matchedRules": [
"PREPAYMENT_KEYWORD",
"EXTERNAL_CONTACT_KAKAO"
],
"reason": "μ΄μμ μ±
μ κ²ν κ° νμν ν€μλκ° κ°μ§λμμ΅λλ€."
}| Method | Endpoint | μ€λͺ |
|---|---|---|
| POST | /api/ops/rules |
μ΄μμ μ± λ£° λ±λ‘ |
| GET | /api/ops/rules |
μ΄μμ μ± λ£° λͺ©λ‘ μ‘°ν |
| PATCH | /api/ops/rules/{id}/status |
μ΄μμ μ± λ£° νμ±ν/λΉνμ±ν |
GET /api/ops/moderation-logs?limit=50&offset=0λμΌ μ½ν μΈ μ λν λ°λ³΅ κ²μ μμ²μ Redis μΊμλ₯Ό ν΅ν΄ DB λ£° μ‘°νμ μν μ μ κ³μ°μ μλ΅ν©λλ€.
| νλͺ© | λ΄μ© |
|---|---|
| Hash Source | title + content + price + category |
| Hash Algorithm | SHA-256 |
| Redis Key | moderation:content:{contentHash} |
| TTL | 5λΆ |
| Cache Hit | λ£° κ³μ°κ³Ό DB μ‘°ν μλ΅ |
| Cache Miss | DB λ£° κΈ°λ° κ²μ ν Redis μ μ₯ |
moderation:content:3f1e9b3f2a2c9a...
Redis CLI λλ RedisInsightμμ keyκ° μ μ₯λ νλ©΄μ μΊ‘μ²ν©λλ€.
docker compose up --buildμ± μ»¨ν μ΄λ μμ μ Alembic migrationμ μ μ©νκ³ μ΄κΈ° seed ruleμ μ μ₯ν©λλ€.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pytestν μ€νΈλ SQLiteμ fake Redisλ₯Ό μ¬μ©ν΄ μΈλΆ μλΉμ€ μμ΄ μ€νλ©λλ€.
| ν μ€νΈ | κΈ°λ κ²°κ³Ό |
|---|---|
| μ μ κ²μκΈ | ALLOW |
| μ μ κΈ ν¬ν¨ κ²μκΈ | REVIEW |
| μ μ κΈ + μΉ΄ν‘ + κ³μ’ ν¬ν¨ κ²μκΈ | BLOCK λλ κΈ°μ€μ λ§λ κ³ μν νμ |
| λΆλ² ν€μλ ν¬ν¨ κ²μκΈ | BLOCK |
| λμΌ μ½ν μΈ μ¬μμ² | Redis μΊμ μ¬μ© |
κ²μ κ²°κ³Όλ moderation_logs ν
μ΄λΈμ μ μ₯λ©λλ€.
μ΄λ₯Ό ν΅ν΄ μ΄μμλ μ΄λ€ κ²μκΈμ΄ μ΄λ€ λ£°μ μν΄ κ²μλμλμ§ νμΈν μ μμ΅λλ€.
μλ μμΉλ μ€μ μ€ν κ²°κ³Όλ₯Ό λ£λ μμμ
λλ€.
μ€μ μΈ‘μ μ μλ μμ μμΉλ₯Ό λ£μ§ μμ΅λλ€.
| κ²μ¦ νλͺ© | κ²°κ³Ό |
|---|---|
| Docker Compose μ€ν | μ±κ³΅ |
| Swagger μ κ·Ό | μ±κ³΅ |
| μ½ν μΈ κ²μ API | μ±κ³΅ |
| μ΄μμ μ± λ£° λ±λ‘/μ‘°ν/μμ | μ±κ³΅ |
| κ²μ λ‘κ·Έ μ μ₯ | μ±κ³΅ |
| Redis μΊμ μ μ₯ | μ±κ³΅ |
| pytest ν΅κ³Ό | μ±κ³΅ |
| μΊ‘μ² | νμΌ κ²½λ‘ |
|---|---|
| Swagger λ¬Έμ | docs/images/swagger-docs.png |
| κ²μ API μλ΅ | docs/images/moderation-check-response.png |
| μ΄μμ μ± λ£° μ‘°ν | docs/images/rules-list-response.png |
| κ²μ λ‘κ·Έ API | docs/images/moderation-logs-response.png |
| PostgreSQL λ‘κ·Έ ν μ΄λΈ | docs/images/moderation-logs-db.png |
| Redis μΊμ ν€ | docs/images/redis-cache-key.png |
| pytest κ²°κ³Ό | docs/images/pytest-result.png |
| Docker μ€ν | docs/images/docker-compose-up.png |
| λΉκ·Ό μ΄μκ°λ°ν μꡬ | νλ‘μ νΈ μ°κ²° |
|---|---|
| μ΄μ μλν | μ΄μμ μ± λ£° κΈ°λ° μλ κ²μ API |
| μ€νΈ/μ¬κΈ°/μ΄λ·°μ§ νμ§ | μν ν€μλ κΈ°λ° μ μ κ³μ° |
| 건κ°ν μ½ν μΈ λλ¬ | ALLOW, REVIEW, BLOCK νμ |
| λμ©λ νΈλν½ κ³ λ € | Redis μΊμ±μΌλ‘ λμΌ μ½ν μΈ μ¬κ²μ λΉμ© κ°μ |
| DB/μΊμ μ΄ν΄ | PostgreSQL, Redis νμ© |
| AI κ΄μ¬ λ° νμ₯μ± | pgvector, AI 보쑰 νλ¨ νμ₯ ꡬ쑰 μ μ |
| μ΄μ νλ¨ κ·Όκ±° | matchedRules, reason, moderation_logs μ μ₯ |
νμ¬λ ν€μλ κΈ°λ° κ²μ λ°©μμ
λλ€.
μΆνμλ pgvectorλ₯Ό νμ©ν΄ κΈ°μ‘΄ μν 문ꡬμ μ μ¬ν ννλ νμ§ν μ μμ΅λλ€.
μμ:
| κΈ°μ‘΄ μν 문ꡬ | μ°ν νν |
|---|---|
| μ μ κΈνλ©΄ 보λ΄λ릴κ²μ | λ¨Όμ 보λ΄μ£Όμλ©΄ νλ°° μ μν κ²μ |
| μΉ΄ν‘ μ£ΌμΈμ | γ γ μ£ΌμΈμ |
| κ³μ’λ‘ λ³΄λ΄μ£ΌμΈμ | γ±γ λ‘ λ³΄λ΄μ£ΌμΈμ |
λͺ¨λ κ²μκΈμ AIλ‘ νλ¨νλ©΄ λΉμ©κ³Ό μλ΅ μκ°μ΄ μ¦κ°ν μ μμ΅λλ€.
λ°λΌμ REVIEW νμ κ²μκΈλ§ AI 보쑰 νλ¨ λμμΌλ‘ λκΈ°λ λ°©μμΌλ‘ νμ₯ν μ μμ΅λλ€.
ALLOW β μ¦μ ν΅κ³Ό
REVIEW β AI 보쑰 νλ¨ λλ μ΄μμ κ²μ
BLOCK β μ°¨λ¨ λλ κ°ν κ²μ ν λ±λ‘
μ¬μ©μ μ κ³ κ° λ€μ΄μ€λ©΄ κΈ°μ‘΄ κ²μ λ‘κ·Έμ μ°κ²°ν΄ μ΄μμ κ²μ μ°μ μμλ₯Ό λμΌ μ μμ΅λλ€.
μ¬μ©μ μ κ³
β κΈ°μ‘΄ moderation_log μ‘°ν
β μν μ μ μ¬κ³μ°
β μ΄μμ κ²μ ν λ±λ‘
κ²μ API μ₯μ λ λΉμ μμ μΈ BLOCK κΈμ¦ μν©μ μ΄μ μ±λλ‘ μ릴 μ μμ΅λλ€.
| 쑰건 | μλ¦Ό |
|---|---|
| API μ€ν¨μ¨ μ¦κ° | μ₯μ μλ¦Ό |
| p95 μλ΅ μκ° μ¦κ° | μ±λ₯ μ ν μλ¦Ό |
| BLOCK νμ κΈμ¦ | μ΄μ ν¨ν΄ μλ¦Ό |
μ΄μμκ° μ§μ λ£°μ λ±λ‘/μμ /λΉνμ±νν μ μλ κ΄λ¦¬μ νλ©΄μΌλ‘ νμ₯ν μ μμ΅λλ€.
curl -X POST http://localhost:8000/api/moderations/check \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"title": "μμ΄ν° μΈκ² νλλ€",
"content": "μ μ
κΈνλ©΄ νλ°° 보λ΄λλ €μ. μΉ΄ν‘ μ£ΌμΈμ.",
"price": 100000,
"category": "DIGITAL"
}'curl http://localhost:8000/api/ops/rulescurl -X POST http://localhost:8000/api/ops/rules \
-H "Content-Type: application/json" \
-d '{
"ruleName": "WIRE_TRANSFER_KEYWORD",
"keyword": "μ‘κΈ",
"score": 30,
"action": "REVIEW",
"category": "FRAUD",
"enabled": true
}'curl -X PATCH http://localhost:8000/api/ops/rules/1/status \
-H "Content-Type: application/json" \
-d '{"enabled": false}'curl "http://localhost:8000/api/ops/moderation-logs?limit=20&offset=0"μ΄ νλ‘μ νΈλ λ¨μν ν€μλ νν°λ§μ΄ μλλΌ, μ΄μμ μ± μ λ°μ΄ν°ννκ³ μλννλ λ°±μλ ꡬ쑰λ₯Ό ꡬνν νλ‘μ νΈμ λλ€.
μ΄μμλ λ£°μ μ§μ μΆκ°νκ±°λ λΉνμ±νν μ μκ³ , κ²μ κ²°κ³Όλ λ‘κ·Έλ‘ μ μ₯λμ΄ μ¬ν λΆμμ΄ κ°λ₯ν©λλ€. λν Redis μΊμ±μ ν΅ν΄ λ°λ³΅ μμ² λΉμ©μ μ€μμΌλ©°, μΆν pgvector κΈ°λ° μ μ¬ λ¬Έκ΅¬ νμ§, AI 보쑰 νλ¨, μ κ³ μμ€ν , μ₯μ μλ¦ΌμΌλ‘ νμ₯ν μ μμ΅λλ€.















