Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dbbbf70
[HSC-226] fix: μ•„ν˜Έμ½”λΌμ‹ 쀑볡 count λ°©μ§€
YeongHyeonHeo Mar 7, 2026
12141b5
[HSC-226] feat: νšŒμ› κΈ°μ€€ 뢄석 κ²°κ³Ό 집계기(aggregator.py) κ΅¬ν˜„
YeongHyeonHeo Mar 7, 2026
334ba85
[HSC-226] test: E2E ν…ŒμŠ€νŠΈ μΆ”κ°€
YeongHyeonHeo Mar 7, 2026
b9b65de
[HSC-226] refactor: 디버깅 print μ£Όμ„μ²˜λ¦¬
YeongHyeonHeo Mar 7, 2026
80f7ccb
[HSC-228] feat: Kafka μ†ŒλΉ„ 기반 SQL 뢄석 νŒŒμ΄ν”„λΌμΈ ꡬ좕
tkv00 Mar 7, 2026
bd9e040
[HSC-228] refactor: REST API 및 EFS 기반 λ ˆκ±°μ‹œ 경둜 제거
tkv00 Mar 7, 2026
d51b10c
Merge pull request #14 from one-year-gap/feat/HSC-226
tkv00 Mar 7, 2026
9c9cca4
[HSC-235] chore: μ„€μ • 파일
rettooo Mar 9, 2026
d5d3bb1
[HSC-235] feat: μΆ”μ²œ κΈ°λŠ₯ api
rettooo Mar 9, 2026
c01b78e
[HSC-235] feat: μƒν’ˆ μž„λ² λ”© 슀크립트
rettooo Mar 9, 2026
7198454
Merge branch 'dev' of https://github.com/one-year-gap/counseling-anal…
tkv00 Mar 9, 2026
f4c3ebb
Merge pull request #16 from one-year-gap/feat/HSC-228
tkv00 Mar 9, 2026
12e743e
[HSC-235] fix: 버전 λͺ…μ‹œ 및, μž„λ² λ”© 배치 λ¦¬νŒ©ν† λ§
rettooo Mar 9, 2026
04a54e7
Merge branch 'dev' into feat/HSC-235, resolve conflicts
rettooo Mar 9, 2026
eddb690
Merge pull request #20 from one-year-gap/feat/HSC-235
tkv00 Mar 9, 2026
299cc0d
[HSC-244] refactor: split realtime api entrypoint
tkv00 Mar 9, 2026
7045396
[HSC-244] feat: add batch runtime and postgres env resolution
tkv00 Mar 9, 2026
b0e9533
[HSC-244] build: support realtime and batch docker modes
tkv00 Mar 9, 2026
7246f07
[HSC-244] fix: kafka message log μ„€μ • μΆ”κ°€
tkv00 Mar 9, 2026
2eaf5f9
Merge pull request #22 from one-year-gap/refactor/HSC-244
tkv00 Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ __pycache__
.env
tests
README.md
node_modules
package-lock.json
pnpm-lock.yaml
e2e_test_efs
e2e_test_efs_agg
36 changes: 36 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Runtime
APP_ENV=local
APP_NAME=recommendation-server
APP_MODE=realtime
APP_PORT=8000
LOG_LEVEL=INFO

# Database (Java λ ˆν¬μ™€ 동일 DB)
# 1) λͺ…μ‹œ URL (ꢌμž₯: 둜컬 개발)
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/holliverse
# DB_URL도 동일 의미둜 μ‚¬μš© κ°€λŠ₯

# 2) ECS/Secrets Manager λΆ„ν•΄ μ£Όμž… (POSTGRES_DSN λ―Έμ„€μ • μ‹œ μžλ™ μ‘°ν•©)
# - Secret JSON key(username/password)λ₯Ό POSTGRES_USER/POSTGRES_PASSWORD둜 λ§€ν•‘ν•΄ μ£Όμž…
# - host/port/dbλŠ” 일반 env둜 μ£Όμž…
# POSTGRES_HOST=your-instance.xxxxx.ap-northeast-2.rds.amazonaws.com
# POSTGRES_PORT=5432
# POSTGRES_DB=holliverse
# POSTGRES_USER=holliverse
# POSTGRES_PASSWORD=*****
# POSTGRES_SSLMODE=require

# 3) 배치용 raw DSN (asyncpg)
# POSTGRES_DSN=postgresql://USER:PASSWORD@HOST:5432/DBNAME?sslmode=require

# RDS μ—°κ²° μ‹œ SSL ν•„μš”ν•˜λ©΄ true (κΈ°λ³Έ false)
DATABASE_SSL=false

# OpenAI
OPENAI_API_KEY=your_openai_api_key
OPENAI_CHAT_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small

# Recommendation
RECOMMEND_TOP_K=3
CACHE_TTL_DAYS=7
28 changes: 26 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# IDE
.idea
.venv
.env

# Python
__pycache__/
*.pyc
*.pyo
*.pyd
dist/
build/
*.egg-info/
.eggs/

# Environment
.venv
.env

# Logs
*.log
logs/

# Node
node_modules/

# OS
.DS_Store

# E2E test data (keep out of git)
e2e_test_efs/**
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pnpm test
45 changes: 40 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
FROM python:3.11-slim
FROM python:3.11-slim AS builder

WORKDIR /app
WORKDIR /build

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential python3-dev \
&& pip wheel --wheel-dir /wheels -r requirements.txt \
&& rm -rf /var/lib/apt/lists/*


FROM python:3.11-slim AS runtime

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
APP_MODE=realtime \
APP_HOST=0.0.0.0 \
APP_PORT=8000

RUN addgroup --system appgroup \
&& adduser --system --ingroup appgroup --home /app appuser

COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* \
&& python -m spacy download ko_core_news_sm \
&& rm -rf /wheels

COPY app ./app
COPY scripts ./scripts
COPY docker-entrypoint.sh ./docker-entrypoint.sh

RUN chmod +x /app/docker-entrypoint.sh \
&& chown -R appuser:appgroup /app

USER appuser

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

ENTRYPOINT ["/app/docker-entrypoint.sh"]
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# counseling-analytics

ν•˜μ΄
FastAPI 개발 μ„œλ²„ μ‹€ν–‰ κ°€μ΄λ“œμž…λ‹ˆλ‹€.

## Prerequisites
Expand Down Expand Up @@ -56,7 +56,13 @@ pip install -r requirements.txt
## 5) FastAPI μ„œλ²„ μ‹€ν–‰

```bash
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
python -m uvicorn app.realtime.main:app --reload --host 0.0.0.0 --port 8000
```

## 5-1) 배치 1회 μ‹€ν–‰

```bash
python -m app.batch.main
```

## 6) 접속 확인
Expand Down
24 changes: 0 additions & 24 deletions app/api/deps.py

This file was deleted.

11 changes: 0 additions & 11 deletions app/api/router.py

This file was deleted.

38 changes: 0 additions & 38 deletions app/api/v1/analyze.py

This file was deleted.

37 changes: 0 additions & 37 deletions app/api/v1/health.py

This file was deleted.

20 changes: 0 additions & 20 deletions app/api/v1/ops.py

This file was deleted.

88 changes: 88 additions & 0 deletions app/batch/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""One-off Kafka batch entrypoint for keyword mapping/extraction."""

from __future__ import annotations

import asyncio
import json
import logging

from aiokafka import AIOKafkaConsumer

from app.core.config import get_settings
from app.core.logging import configure_logging
from app.infra.postgres.analysis_repository import AnalysisRepository
from app.infra.postgres.client import create_postgres_pool
from app.infra.postgres.dispatch_outbox_repository import DispatchOutboxRepository
from app.schemas.analysis_request_message import AnalysisRequestMessage
from app.services.kafka_analysis_consumer_service import KafkaAnalysisConsumerService
from app.services.sql_keyword_analysis_service import SqlKeywordAnalysisService

logger = logging.getLogger(__name__)


async def run_once() -> int:
settings = get_settings()
configure_logging(settings.debug)

service = KafkaAnalysisConsumerService(settings)
service._db_pool = await create_postgres_pool(settings) # noqa: SLF001
service._analysis_repository = AnalysisRepository(service._db_pool) # noqa: SLF001
service._outbox_repository = DispatchOutboxRepository(service._db_pool) # noqa: SLF001
service._analysis_service = SqlKeywordAnalysisService() # noqa: SLF001
Comment on lines +28 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

KafkaAnalysisConsumerService의 μ˜μ‘΄μ„±μ„ μ£Όμž…ν•˜κΈ° μœ„ν•΄ _db_poolκ³Ό 같은 보호된(protected) 멀버에 직접 μ ‘κ·Όν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” μΊ‘μŠν™”λ₯Ό μœ„λ°˜ν•˜λ©° μ½”λ“œμ˜ μœ μ§€λ³΄μˆ˜μ„±μ„ λ–¨μ–΄λœ¨λ¦½λ‹ˆλ‹€. noqa: SLF001 주석이 이λ₯Ό μ•”μ‹œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

KafkaAnalysisConsumerService의 __init__ λ©”μ„œλ“œλ₯Ό 톡해 μ˜μ‘΄μ„±μ„ λͺ…μ‹œμ μœΌλ‘œ μ£Όμž…ν•˜λ„λ‘ λ¦¬νŒ©ν„°λ§ν•˜λŠ” 것을 μ œμ•ˆν•©λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ 클래슀의 μ—­ν• κ³Ό μ±…μž„μ΄ 더 λͺ…ν™•ν•΄μ§€κ³  ν…ŒμŠ€νŠΈν•˜κΈ° μ‰¬μš΄ ꡬ쑰가 λ©λ‹ˆλ‹€.


consumer = AIOKafkaConsumer(
settings.kafka_analysis_request_topic,
bootstrap_servers=[s.strip() for s in settings.kafka_bootstrap_servers.split(",") if s.strip()],
group_id=settings.kafka_consumer_group_id,
auto_offset_reset=settings.kafka_auto_offset_reset,
enable_auto_commit=False,
value_deserializer=lambda value: json.loads(value.decode("utf-8")),
)

processed_count = 0
received_count = 0
dropped_count = 0
try:
await consumer.start()
polled = await consumer.getmany(
timeout_ms=settings.kafka_poll_timeout_ms,
max_records=settings.kafka_batch_size,
)

messages: list[AnalysisRequestMessage] = []
for _, records in polled.items():
for record in records:
received_count += 1
parsed = service._parse_message(record.value) # noqa: SLF001
if parsed is None:
dropped_count += 1
continue
messages.append(parsed)

if messages:
for chunk in service._chunk(messages, settings.kafka_batch_size): # noqa: SLF001
await service._process_batch(chunk) # noqa: SLF001
processed_count += len(chunk)
await consumer.commit()
elif received_count > 0 and dropped_count == received_count:
await consumer.commit()

logger.info(
"Keyword batch run finished once. received=%d dropped=%d processed=%d",
received_count,
dropped_count,
processed_count,
)
return processed_count
finally:
await consumer.stop()
if service._db_pool is not None: # noqa: SLF001
await service._db_pool.close() # noqa: SLF001


def main() -> None:
asyncio.run(run_once())


if __name__ == "__main__":
main()
Loading
Loading