Spaces:
Runtime error
Runtime error
๋จ๊ณ๋ณ ๊ฐ๋ฐ ๊ณํ์
๊ธ์ต๊ณต์ ํฌ๋กค๋ง + ๋ถ์ ์๋ ๋งค์นญ ์๋น์ค
๊ธฐ์ค ๋ฌธ์: PRD v1.0
์์ฑ์ผ: 2026-06-17
์ด ๊ฐ๋ฐ ๊ธฐ๊ฐ: 13์ฃผ
์ ์ฒด ์ผ์ ๊ฐ์
Week 1 2 3 4 5 6 7 8 9 10 11 12 13
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Step1 โโโโโโโโโโโโ
Step2 โโโโโโโโโโโโ
Step3 โโโโโโโโ
Step4 โโโโโโโโ
Step5 โโโโโโโโ
| ๋จ๊ณ | ์ด๋ฆ | ๊ธฐ๊ฐ | ์ฐ์ถ๋ฌผ |
|---|---|---|---|
| Step 1 | ๋ฐ์ดํฐ ์์ง ํ์ดํ๋ผ์ธ | 1~3์ฃผ | ํฌ๋กค๋ฌ + DB + ์ค์ผ์ค๋ฌ |
| Step 2 | ๋ถ์ ๋งค์นญ ๋ชจ๋ธ ์๋น | 3~5์ฃผ | ๋ชจ๋ธ API ์๋ฒ + ๋งค์นญ ์์ปค |
| Step 3 | ๋ฐฑ์๋ API ์๋ฒ | 5~7์ฃผ | REST API + ์ธ์ฆ |
| Step 4 | ํ๋ก ํธ์๋ ๊ฐ๋ฐ | 7~10์ฃผ | ์น UI + Telegram Bot |
| Step 5 | ํตํฉ ํ ์คํธ ๋ฐ ์ด์ ์์ ํ | 10~13์ฃผ | ์ ์ฒด ํตํฉ + ๋ชจ๋ํฐ๋ง |
Step 1. ๋ฐ์ดํฐ ์์ง ํ์ดํ๋ผ์ธ ๊ตฌ์ถ (Week 1~3)
๋ชฉํ
์ธ๋ถ ๊ธฐ๊ด ๊ณต์๋ฅผ DB์ ์ ๋ขฐ์ฑ ์๊ฒ ์ ์ฌํ๋ ์๋ํ ํ์ดํ๋ผ์ธ ์์ฑ
1-1. ๊ฐ๋ฐ ํ๊ฒฝ ๋ฐ ์ธํ๋ผ ์ ์ (Week 1)
์์ ํญ๋ชฉ
[ ] Docker Compose ๊ธฐ๋ฐ ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ
- PostgreSQL 16
- Redis (๋ฉ์์ง ํ ์ฉ๋)
- Adminer (DB ๊ด๋ฆฌ UI)
[ ] ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ์์ฑ
- announcements ํ
์ด๋ธ
- dept_matches ํ
์ด๋ธ
- departments ํ
์ด๋ธ
- crawl_logs ํ
์ด๋ธ
- Alembic ๋ง์ด๊ทธ๋ ์ด์
์ค์
[ ] Python ํ๋ก์ ํธ ๊ตฌ์กฐ ์ด๊ธฐํ
- poetry ๋๋ pip-tools ์์กด์ฑ ๊ด๋ฆฌ
- ๊ธฐ๋ณธ ์ค์ ํ์ผ (config.py, .env)
- ๋ก๊น
์ค์ (structlog ๋๋ loguru)
๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
project/
โโโ crawler/
โ โโโ base.py # BaseCrawler ์ถ์ ํด๋์ค
โ โโโ agencies/
โ โ โโโ fsc.py # ๊ธ์ต์์ํ
โ โ โโโ fss.py # ๊ธ์ต๊ฐ๋
์
โ โ โโโ kofiu.py # ๊ธ์ต์ ๋ณด๋ถ์์
โ โ โโโ moleg.py # ๋ฒ์ ์ฒ
โ โ โโโ bok.py # ํ๊ตญ์ํ
โ โโโ parser.py # HTML โ ์ ์ ํ
์คํธ ๊ณตํต ์ ํธ
โโโ db/
โ โโโ models.py # SQLAlchemy ORM ๋ชจ๋ธ
โ โโโ session.py # DB ์ธ์
๊ด๋ฆฌ
โ โโโ migrations/ # Alembic ๋ง์ด๊ทธ๋ ์ด์
โโโ scheduler/
โ โโโ tasks.py # APScheduler or Celery beat ์์
์ ์
โโโ matching/ # Step 2์์ ์ฑ์
โโโ api/ # Step 3์์ ์ฑ์
โโโ docker-compose.yml
ํต์ฌ ์ค๊ณ ์์น
BaseCrawler์ถ์ ํด๋์ค๋ก ๊ธฐ๊ด๋ณ ํฌ๋กค๋ฌ๋ฅผ ํ๋ฌ๊ทธ์ธ ๋ฐฉ์์ผ๋ก ํ์ฅ- ๊ธฐ๊ด ์ถ๊ฐ ์
agencies/ํ์ ํ์ผ๋ง ์ถ๊ฐํ๋ฉด ์ค์ผ์ค๋ฌ ์๋ ๋ฑ๋ก
1-2. ํฌ๋กค๋ฌ ๊ตฌํ (Week 1~2)
BaseCrawler ์ธํฐํ์ด์ค
class BaseCrawler(ABC):
agency_name: str # "๊ธ์ต์์ํ"
interval_hours: int # 1 or 2
base_url: str
@abstractmethod
def fetch_list(self) -> list[dict]:
"""๋ชฉ๋ก ํ์ด์ง์์ ์ ๋ชฉ/URL/๋ ์ง ์์ง"""
@abstractmethod
def fetch_detail(self, url: str) -> dict:
"""์์ธ ํ์ด์ง์์ ๋ณธ๋ฌธ/๋ด๋น๋ถ์/์ฐ๋ฝ์ฒ ์์ง"""
def run(self) -> int:
"""์ ๊ท ๊ฑด๋ง ์์งํ์ฌ DB ์ ์ฅ, ์์ง ๊ฑด์ ๋ฐํ"""
๊ธฐ๊ด๋ณ ๊ตฌํ ํฌ์ธํธ
| ๊ธฐ๊ด | ์์ง ๋ฐฉ์ | ์ฃผ์ ํ์ฑ ํฌ์ธํธ |
|---|---|---|
| ๊ธ์ต์์ํ (fsc.go.kr) | requests + BeautifulSoup | ๋ณด๋์๋ฃ/๊ณ ์๊ณต๊ณ /์๊ฒฐ๊ฒฐ๊ณผ ๊ฒ์ํ ๋ถ๋ฆฌ |
| ๊ธ์ต๊ฐ๋ ์ (fss.or.kr) | requests + BeautifulSoup | ์นดํ ๊ณ ๋ฆฌ๋ณ ๋ชฉ๋ก URL ํจํด ํ์ธ ํ์ |
| KoFIU (kofiu.go.kr) | requests + BeautifulSoup | JS ๋ ๋๋ง ์ฌ๋ถ ์ฌ์ ํ์ธ, Playwright fallback |
| ๋ฒ์ ์ฒ (moleg.go.kr) | requests + BeautifulSoup | ๋ฒ๋ นํด์/์๊ฒฌ์ 2๊ฐ ๊ฒ์ํ |
| ํ๊ตญ์ํ (bok.or.kr) | requests + BeautifulSoup | ๊ธํต์ ์๊ฒฐ ๋ณ๋ ์นดํ ๊ณ ๋ฆฌ |
๊ณตํต ์ฒ๋ฆฌ ๋ก์ง
# ์ค๋ณต ๋ฐฉ์ง: URL unique constraint + upsert
# ์ ๊ท ๊ฑด ํ๋ณ: ๋ง์ง๋ง ์์ง published_at ์ดํ ํญ๋ชฉ๋ง ์ฒ๋ฆฌ
# ๋ณธ๋ฌธ ์ ์ : HTML ํ๊ทธ ์ ๊ฑฐ, ์ฐ์ ๊ณต๋ฐฑ/์ค๋ฐ๊ฟ ์ ๊ทํ
# ๋ด๋น๋ถ์ ์ถ์ถ: "๋ด๋น๋ถ์[:\s]+([๊ฐ-ํฃ]+[ํ๊ณผ์ค๋ถ์ฒ])" ํจํด ์ ๊ท์
# ์ฐ๋ฝ์ฒ ์ถ์ถ: "02-\d{4}-\d{4}" ํจํด ์ ๊ท์
# ์์ฒญ ๋๋ ์ด: ๊ธฐ๊ด๋น 1~3์ด random sleep
# ์๋ฌ ์ฒ๋ฆฌ: ์ง์ ๋ฐฑ์คํ ์ฌ์๋ (1s โ 2s โ 4s, ์ต๋ 3ํ)
์์ ํญ๋ชฉ
[ ] BaseCrawler ์ถ์ ํด๋์ค ๊ตฌํ
[ ] ๊ธ์ต์์ํ ํฌ๋กค๋ฌ (fsc.py) โ ์ฐ์ ์์ 1
[ ] ๊ธ์ต๊ฐ๋
์ ํฌ๋กค๋ฌ (fss.py) โ ์ฐ์ ์์ 2
[ ] HTML ์ ์ ์ ํธ (parser.py)
[ ] ๋ด๋น๋ถ์/์ฐ๋ฝ์ฒ ์ ๊ท์ ์ถ์ถ๊ธฐ
[ ] ์ค๋ณต ๊ฐ์ง + DB upsert ๋ก์ง
[ ] ๋จ์ ํ
์คํธ: ํฌ๋กค๋ฌ๋ณ ํ์ฑ ์ ํ๋ ๊ฒ์ฆ
1-3. ์ค์ผ์ค๋ฌ ๋ฐ ๋ฉ์์ง ํ (Week 2~3)
์ค์ผ์ค๋ฌ ๊ตฌ์ฑ (APScheduler)
# scheduler/tasks.py
scheduler.add_job(FscCrawler().run, 'interval', hours=1, id='fsc')
scheduler.add_job(FssCrawler().run, 'interval', hours=1, id='fss')
scheduler.add_job(KofiuCrawler().run, 'interval', hours=1, id='kofiu')
scheduler.add_job(MolegCrawler().run, 'interval', hours=2, id='moleg')
scheduler.add_job(BokCrawler().run, 'interval', hours=2, id='bok')
๋ฉ์์ง ํ (Redis Stream)
ํฌ๋กค๋ฌ โ XADD announcements_stream {announcement_id}
๋งค์นญ ์์ปค โ XREAD ์๋น ํ predict_department() ํธ์ถ
์์ ํญ๋ชฉ
[ ] APScheduler + Redis ์ฐ๋ ์ค์
[ ] ํฌ๋กค๋ฌ ์๋ฃ ํ Redis Stream์ ์ด๋ฒคํธ ๋ฐํ
[ ] crawl_logs ํ
์ด๋ธ ์๋ ๊ธฐ๋ก (์์/์ข
๋ฃ/์์ง๊ฑด์/์ค๋ฅ)
[ ] ํฌ๋กค๋ง ์คํจ ์ Slack/์ด๋ฉ์ผ ์ค๋ฅ ์๋ฆผ (์ ํ)
[ ] ๋๋จธ์ง 3๊ฐ ๊ธฐ๊ด ํฌ๋กค๋ฌ (kofiu, moleg, bok)
Step 1 ์๋ฃ ๊ธฐ์ค (Definition of Done)
- 5๊ฐ ๊ธฐ๊ด ํฌ๋กค๋ฌ ๋ชจ๋ ๋์, ์ ๊ท ๊ณต์ ์๋ DB ์ ์ฌ ํ์ธ
- 24์๊ฐ ๋ฌด์ค๋จ ์ค์ผ์ค ์คํ ์์ ์ฑ ํ์ธ
- ์ค๋ณต ์์ง 0๊ฑด ํ์ธ (URL unique ์ ์ฝ ๊ฒ์ฆ)
- crawl_logs ๊ธฐ๋ก ์ ์ ํ์ธ
Step 2. ๋ถ์ ๋งค์นญ ๋ชจ๋ธ ์๋น (Week 3~5)
๋ชฉํ
v3.ipynb์ Hybrid Retrieval ๋ชจ๋ธ์ ์ด์ ๊ฐ๋ฅํ API ์๋น์ค๋ก ์ ํํ๊ณ , ํฌ๋กค๋ง๋ ๊ณต์์ ๋ถ์๋ฅผ ์๋ ๋งค์นญ
2-1. ๋ชจ๋ธ ํ์ต ์ฌํ ๋ฐ ๊ฒ์ฆ (Week 3)
์์ ํญ๋ชฉ
[ ] v3.ipynb ๋ก์ปฌ/Colab ์คํ ํ๊ฒฝ ์ฌํ
- ์์กด์ฑ: sentence-transformers, rank_bm25, python-docx, openpyxl
- ํ์ต ๋ฐ์ดํฐ: ์
๋ฌด๋ด๋น๊ท์ .docx, dept.xlsx ๊ฒฝ๋ก ํ์ธ
[ ] Stage 1 ํ์ต ์คํ
- ์
๋ฌด๋ด๋น๊ท์ .docx โ self-positive ํ์ต์ ์์ฑ
- jhgan/ko-sroberta-multitask ๋๋ฉ์ธ ์ ์
- ๊ฒฐ๊ณผ: stage1_domain_retriever/ ์ ์ฅ
[ ] Stage 2 ํ์ต ์คํ
- dept.xlsx โ ๋ถ์๋ณ 5ํ chunk ์์ฑ
- OFWRK_NM โ chunk ๋งคํ ํ์ต
- ๊ฒฐ๊ณผ: stage2_dept_retriever/ ์ ์ฅ
[ ] ๊ธฐ์กด query1~3.txt๋ก ์ถ๋ก ํ
์คํธ โ ์์ ๋ถ์ ๊ฒ์ฆ
[ ] ํ์ต๋ ๋ชจ๋ธ ํ์ผ ๋ฒ์ ํ๊น
(์: v3_20260617)
๋ชจ๋ธ ์ฑ๋ฅ ๊ธฐ์ค ๊ฒ์ฆ
# ํ
์คํธ ์ผ์ด์ค (query1~3 ๊ธฐ์ค ์๋ ๊ฒ์ฆ)
query1 โ ์์: ๋์งํธ๊ธ์ต์ด๊ด๊ณผ or ํํ
ํฌํ์ ๊ณผ (ํ์ ๊ธ์ต์๋น์ค)
query2 โ ์์: ์๋ฏผ๊ธ์ต๊ณผ (์ฑ๊ถ๋งค๊ฐ/์ฐ์ฒด์ฑ๋ฌด)
query3 โ ์์: ๊ธ์ต์๋น์์ ์ฑ
๊ณผ (๊ธ์ต๊ต์ก)
2-2. ๋ชจ๋ธ FastAPI ์๋ฒ ๊ตฌํ (Week 4)
๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
matching/
โโโ model.py # ๋ชจ๋ธ ๋ก๋ฉ + predict_department() ๋ํผ
โโโ server.py # FastAPI ์ฑ ์ ์
โโโ schemas.py # Pydantic ์
์ถ๋ ฅ ์คํค๋ง
โโโ worker.py # Redis Stream ์๋น ์์ปค
API ์๋ํฌ์ธํธ
# POST /match
# Request
{
"text": "๋ง์ด๋ฐ์ดํฐ ํ์ฉ ๊ธ๋ฆฌ์ธํ์๊ตฌ ์๋น์ค ํ์ ๊ธ์ต 3๊ฑด ์ ๊ท ์ง์ ...",
"top_n": 5
}
# Response
{
"final_department": "๋์งํธ๊ธ์ต์ด๊ด๊ณผ",
"confidence_score": 0.87,
"top5_candidates": [
{"rank": 1, "dept": "๋์งํธ๊ธ์ต์ด๊ด๊ณผ", "score": 0.87},
{"rank": 2, "dept": "ํํ
ํฌํ์ ๊ณผ", "score": 0.71},
{"rank": 3, "dept": "์ ์๊ธ์ต๊ณผ", "score": 0.65},
{"rank": 4, "dept": "IT๊ธ์ต์ ๋ณด๋ณดํธํ", "score": 0.60},
{"rank": 5, "dept": "๊ธ์ตํ์ ์ง์์ค", "score": 0.54}
],
"processing_time_ms": 118,
"model_version": "v3_20260617"
}
๋ชจ๋ธ ์๋ฒ ์ค๊ณ ํฌ์ธํธ
# ์ฑ ์์ ์ 1ํ ๋ก๋ (startup ์ด๋ฒคํธ)
@app.on_event("startup")
async def load_model():
app.state.embedder = SentenceTransformer(STAGE2_MODEL_DIR)
app.state.corpus_embs = ... # pre-computed embeddings
app.state.tfidf = ...
app.state.bm25 = ...
# ์ ๋ขฐ๋ fallback ์ฒ๋ฆฌ
if result["confidence_score"] < 0.4:
result["final_department"] = None
result["needs_manual_review"] = True
์์ ํญ๋ชฉ
[ ] model.py: predict_department() ํจ์ v3.ipynb์์ ์ถ์ถ/์ ๋ฆฌ
[ ] schemas.py: MatchRequest / MatchResponse Pydantic ๋ชจ๋ธ
[ ] server.py: FastAPI ์ฑ + /match + /health ์๋ํฌ์ธํธ
[ ] Docker ์ด๋ฏธ์ง ๋น๋ (python:3.11-slim ๊ธฐ๋ฐ)
[ ] /match ์๋ํฌ์ธํธ ๋ถํ ํ
์คํธ (๋ชฉํ: โค500ms/๊ฑด)
2-3. ๋งค์นญ ์์ปค ๊ตฌํ (Week 4~5)
์์ปค ๋์ ๋ฐฉ์
Redis Stream 'announcements_stream' ๊ตฌ๋
โ
announcement_id ์์
โ
announcements ํ
์ด๋ธ์์ body_text ์กฐํ
โ
POST /match ํธ์ถ (๋๋ ์ง์ predict_department ํธ์ถ)
โ
dept_matches ํ
์ด๋ธ INSERT
โ
announcements.is_matched = TRUE ์
๋ฐ์ดํธ
โ
์ ๋ขฐ๋ < 0.4์ด๋ฉด โ manual_review_queue์ ์ถ๊ฐ
์์ ํญ๋ชฉ
[ ] worker.py: Redis Stream XREAD ๋ฃจํ ๊ตฌํ
[ ] ๋งค์นญ ๊ฒฐ๊ณผ dept_matches ํ
์ด๋ธ ์ ์ฅ
[ ] ์ ๋ขฐ๋ ์๊ณ๊ฐ(0.4) ๋ฏธ๋ง ์ฒ๋ฆฌ ๋ก์ง
[ ] ๋ฐฐ์น ์ฒ๋ฆฌ: ๋ฐฑ๋ก๊ทธ ๋์ ์ batch_predict() ํ์ฉ
[ ] ์์ปค ์ฌ์์ ์ ๋ฏธ์ฒ๋ฆฌ ๊ฑด ์๋ ์ฌ์ฒ๋ฆฌ (XPENDING ํ์ฉ)
[ ] ํตํฉ ํ
์คํธ: ํฌ๋กค๋ฌ ์์ง โ ์์ปค ๋งค์นญ โ DB ์ ์ฅ E2E ํ์ธ
Step 2 ์๋ฃ ๊ธฐ์ค (Definition of Done)
- ๋ชจ๋ธ API /match ์๋ต ์๊ฐ โค 500ms (CPU ํ๊ฒฝ)
- query1~3 ์์ ๋ถ์ ์ ํํ ๋ฐํ ํ์ธ
- ํฌ๋กค๋ฌ ์์ง ํ 5๋ถ ์ด๋ด ๋งค์นญ ๊ฒฐ๊ณผ DB ์ ์ฌ ํ์ธ
- ์ ๋ขฐ๋ < 0.4 ๊ฑด manual_review ์ฒ๋ฆฌ ํ์ธ
- Docker Compose๋ก ๋ชจ๋ธ ์๋ฒ ๋จ๋ ๊ธฐ๋ ๊ฐ๋ฅ
Step 3. ๋ฐฑ์๋ API ์๋ฒ ๊ฐ๋ฐ (Week 5~7)
๋ชฉํ
ํ๋ก ํธ์๋์ ์ธ๋ถ ์ฐ๋์ด ๊ฐ๋ฅํ REST API ์๋ฒ ์์ฑ
3-1. API ์๋ฒ ๊ธฐ๋ณธ ๊ตฌ์กฐ (Week 5~6)
๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ
api/
โโโ main.py # FastAPI ์ฑ ์ํธ๋ฆฌํฌ์ธํธ
โโโ routers/
โ โโโ announcements.py
โ โโโ departments.py
โ โโโ match.py
โโโ schemas/
โ โโโ announcement.py
โ โโโ department.py
โโโ services/
โ โโโ announcement_service.py
โ โโโ match_service.py
โโโ auth/
โ โโโ api_key.py # API Key ์ธ์ฆ ๋ฏธ๋ค์จ์ด
โโโ dependencies.py # DB ์ธ์
๋ฑ ๊ณตํต ์์กด์ฑ
๊ตฌํํ ์๋ํฌ์ธํธ
๊ณต์ ๋ชฉ๋ก ์กฐํ
GET /api/v1/announcements
Query: source_agency, category, dept, range(D|W|M|ALL),
page, per_page, min_confidence
Response: { total, page, items: [...] }
๊ณต์ ์์ธ ์กฐํ
GET /api/v1/announcements/{id}
Response: ์ ์ฒด ํ๋ + ๋งค์นญ Top-5 + ์๋ฌธ ๋ด๋น๋ถ์/์ฐ๋ฝ์ฒ
๋ถ์ ๋ชฉ๋ก ์กฐํ
GET /api/v1/departments
Response: [{ dept_name, is_active }]
์๋ ๋งค์นญ ์ฌ์์ฒญ
POST /api/v1/announcements/{id}/rematch
Body: { "override_dept": "ํํ
ํฌํ์ ๊ณผ" } (์๋ต ์ ๋ชจ๋ธ ์ฌ์ถ๋ก )
Response: ๊ฐฑ์ ๋ ๋งค์นญ ๊ฒฐ๊ณผ
์๋ ๊ฒํ ํ ์กฐํ (์ด์์์ฉ)
GET /api/v1/review-queue
Response: ์ ๋ขฐ๋ < 0.4์ธ ๋ฏธ์ฒ๋ฆฌ ๊ณต์ ๋ชฉ๋ก
์์ ํญ๋ชฉ
[ ] FastAPI ์ฑ ์ด๊ธฐํ + CORS ์ค์
[ ] API Key ์ธ์ฆ ๋ฏธ๋ค์จ์ด (X-API-Key ํค๋)
[ ] GET /announcements ๊ตฌํ (ํํฐ + ํ์ด์ง๋ค์ด์
)
[ ] GET /announcements/{id} ๊ตฌํ
[ ] GET /departments ๊ตฌํ
[ ] POST /announcements/{id}/rematch ๊ตฌํ
[ ] GET /review-queue ๊ตฌํ
[ ] OpenAPI ๋ฌธ์ ์๋ ์์ฑ ํ์ธ (/docs)
3-2. ์ฑ๋ฅ ์ต์ ํ ๋ฐ ๋ณด์ (Week 6~7)
DB ์ฟผ๋ฆฌ ์ต์ ํ
-- ์์ฃผ ์ฌ์ฉ๋๋ ํํฐ ์กฐํฉ์ ์ธ๋ฑ์ค ์ถ๊ฐ
CREATE INDEX idx_ann_agency_published ON announcements(source_agency, published_at DESC);
CREATE INDEX idx_ann_published_range ON announcements(published_at DESC);
CREATE INDEX idx_match_dept ON dept_matches(final_dept);
CREATE INDEX idx_match_confidence ON dept_matches(confidence_score);
-- ๋ชฉ๋ก ์กฐํ์ฉ ๋ทฐ (announcements + dept_matches JOIN)
CREATE VIEW v_announcements_with_match AS
SELECT a.*, dm.final_dept, dm.confidence_score, dm.top5_json
FROM announcements a
LEFT JOIN dept_matches dm ON a.id = dm.announcement_id;
๋ณด์
- API Key ์ธ์ฆ (X-API-Key ํค๋)
- Rate Limiting: 100 req/min per API Key
- HTTPS ๊ฐ์ (nginx ๋ฆฌ๋ฒ์ค ํ๋ก์)
- SQL Injection ๋ฐฉ์ง: SQLAlchemy ORM ์ฌ์ฉ (raw query ๊ธ์ง)
- ์
๋ ฅ๊ฐ ๊ฒ์ฆ: Pydantic ๋ชจ๋ธ๋ก ๋ชจ๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํ์
๊ฒ์ฆ
์์ ํญ๋ชฉ
[ ] ์ฟผ๋ฆฌ ์ฑ๋ฅ ๊ฒ์ฆ (๋ชฉ๋ก ์กฐํ โค 200ms)
[ ] API Key ๋ฐ๊ธ/๊ด๋ฆฌ ํ
์ด๋ธ ์ถ๊ฐ
[ ] Rate Limiting ๋ฏธ๋ค์จ์ด (slowapi)
[ ] ์๋ต ์บ์ฑ: ๋ถ์ ๋ชฉ๋ก (5๋ถ ์บ์, Redis)
[ ] ์ ์ฒด API ์๋ํฌ์ธํธ ํตํฉ ํ
์คํธ (pytest)
[ ] Postman Collection ๋๋ HTTP ํ์ผ ์์ฑ
Step 3 ์๋ฃ ๊ธฐ์ค (Definition of Done)
- ์ ์ฒด ์๋ํฌ์ธํธ ์ ์ ๋์ ๋ฐ Postman ๊ฒ์ฆ ์๋ฃ
- ๋ชฉ๋ก ์กฐํ API ์๋ต โค 200ms (์ธ๋ฑ์ค ์ ์ฉ ๊ธฐ์ค)
- API Key ์๋ ์์ฒญ 401 ๋ฐํ ํ์ธ
- OpenAPI ๋ฌธ์(/docs) ์๋ ์์ฑ ํ์ธ
- pytest ๊ธฐ๋ฐ API ํตํฉ ํ ์คํธ 80% ์ด์ ์ปค๋ฒ๋ฆฌ์ง
Step 4. ํ๋ก ํธ์๋ ๊ฐ๋ฐ (Week 7~10)
๋ชฉํ
๋ด๋น์๊ฐ ๊ณต์์ ๋งค์นญ ๋ถ์๋ฅผ ํ ํ๋ฉด์์ ํ์ธํ๊ณ ํํฐ๋งํ ์ ์๋ ์น UI ์์ฑ
4-1. ๊ธฐ์ ์คํ ๋ฐ ์ด๊ธฐ ์ค์ (Week 7)
ํ๋ ์์ํฌ: Next.js 14 (App Router)
์คํ์ผ๋ง: Tailwind CSS + shadcn/ui
์ํ๊ด๋ฆฌ: Zustand (ํด๋ผ์ด์ธํธ ์ํ)
๋ฐ์ดํฐ ํ์นญ: TanStack Query (์๋ฒ ์ํ)
๋ฐฐํฌ: Vercel ๋๋ Nginx + Docker
ํ๋ก์ ํธ ๊ตฌ์กฐ
frontend/
โโโ app/
โ โโโ page.tsx # ๋ฉ์ธ ํผ๋
โ โโโ announcements/
โ โ โโโ [id]/page.tsx # ์์ธ ํ์ด์ง
โ โโโ dashboard/page.tsx # ๋ถ์๋ณ ๋์๋ณด๋
โโโ components/
โ โโโ AnnouncementCard.tsx # ๊ณต์ ์นด๋ (ํต์ฌ)
โ โโโ DeptMatchBadge.tsx # ๋ถ์ ๋งค์นญ + ์ ๋ขฐ๋ ๋ฐ
โ โโโ FilterBar.tsx # ๊ธฐ๊ด/๊ธฐ๊ฐ/๋ถ์ ํํฐ
โ โโโ AgencyTabs.tsx # ๊ธฐ๊ด ํญ
โ โโโ ReviewAlert.tsx # ์๋ ๊ฒํ ๊ฒฝ๊ณ ๋ฐฐ์ง
โโโ hooks/
โ โโโ useAnnouncements.ts
โ โโโ useDepartments.ts
โโโ lib/
โโโ api.ts # API ํด๋ผ์ด์ธํธ
์์ ํญ๋ชฉ
[ ] Next.js ํ๋ก์ ํธ ์ด๊ธฐํ + Tailwind + shadcn/ui ์ค์
[ ] API ํด๋ผ์ด์ธํธ (lib/api.ts) ํ์
์ ์
[ ] TanStack Query ์ค์ ๋ฐ ์ปค์คํ
ํ
์์ฑ
[ ] ํ๊ฒฝ๋ณ์ ์ค์ (NEXT_PUBLIC_API_URL)
4-2. ํต์ฌ ์ปดํฌ๋ํธ ๊ตฌํ (Week 8~9)
AnnouncementCard (๊ณต์ ์นด๋)
// ํ์ ์ ๋ณด
// - ๊ธฐ๊ด ๋ฐฐ์ง (์์ ๊ตฌ๋ถ: ๊ธ์ต์=ํ๋, ๊ธ๊ฐ์=์ด๋ก, ...)
// - ์นดํ
๊ณ ๋ฆฌ ํ๊ทธ
// - ์ ๋ชฉ (2์ค ๋ง์ค์)
// - ๊ฒ์์ผ์ (์๋์๊ฐ: "3์๊ฐ ์ ")
// - ๋งค์นญ ๋ถ์ + ์ ๋ขฐ๋ ๋ฐ
// - ํ๋ณด ๋ถ์ 2~3๊ฐ (์์ ๊ธ์จ)
// - ์๋ ๊ฒํ ํ์ ๊ฒฝ๊ณ (์ ๋ขฐ๋ < 40%)
// - [์๋ฌธ ๋ณด๊ธฐ] ๋ฒํผ
DeptMatchBadge (๋ถ์ ๋งค์นญ ์๊ฐํ)
// ๋์ (80%+): ์ด๋ก progress bar
// ๋ณดํต (60~79%): ๋
ธ๋ progress bar
// ๋ฎ์ (40~59%): ์ฃผํฉ progress bar
// ๊ฒํ ํ์ (<40%): ๋นจ๊ฐ + "โ ๏ธ ์๋ ๊ฒํ " ํ
์คํธ
FilterBar (ํํฐ ์์ญ)
// - ๊ธฐ๊ด ํญ (์ ์ฒด/๊ธ์ต์/๊ธ๊ฐ์/KoFIU/๋ฒ์ ์ฒ/ํ๊ตญ์ํ)
// ๊ฐ ํญ์ ๋ฏธํ์ธ ๊ฑด์ ๋ฑ์ง
// - ๊ธฐ๊ฐ ํํฐ (D/W/M/ALL) โ ํ ๊ธ ๋ฒํผ ๊ทธ๋ฃน
// - ๋ถ์ ํํฐ ๋๋กญ๋ค์ด (๊ฒ์ ๊ฐ๋ฅ, ๋ด ๋ถ์ ์ ์ฅ ๊ธฐ๋ฅ)
// - ์นดํ
๊ณ ๋ฆฌ ํํฐ ๋๋กญ๋ค์ด
// - ์ ๋ขฐ๋ ์๊ณ๊ฐ ์ฌ๋ผ์ด๋ (0~100%)
์์ ํญ๋ชฉ
[ ] AnnouncementCard ์ปดํฌ๋ํธ ๊ตฌํ
[ ] DeptMatchBadge ์ปดํฌ๋ํธ (์ ๋ขฐ๋ ์์ ๋ถ๊ธฐ)
[ ] FilterBar ์ปดํฌ๋ํธ (๊ธฐ๊ดํญ + ๊ธฐ๊ฐ + ๋ถ์ ๋๋กญ๋ค์ด)
[ ] ๊ณต์ ๋ชฉ๋ก ํ์ด์ง (๋ฌดํ ์คํฌ๋กค or ํ์ด์ง๋ค์ด์
)
[ ] ๊ณต์ ์์ธ ํ์ด์ง (๋ณธ๋ฌธ + Top-5 ํ๋ณด ๋ฐ ์ฐจํธ)
[ ] ๋ถ์๋ณ ๋์๋ณด๋ ํ์ด์ง (๋ด ๋ถ์ ํํฐ ์ ์ฅ)
[ ] ๋ก๋ฉ ์ค์ผ๋ ํค UI
[ ] ๋น ์ํ ์ฒ๋ฆฌ ("์กฐ๊ฑด์ ๋ง๋ ๊ณต์๊ฐ ์์ต๋๋ค")
4-3. Telegram Bot ์ฐ๋ (Week 9~10)
Bot ๋์ ๋ฐฉ์
์ ๊ท ๊ณต์ DB ์ ์ฌ + ๋งค์นญ ์๋ฃ
โ
Redis Stream 'telegram_stream' ์ด๋ฒคํธ ๋ฐํ
โ
Telegram Bot ์์ปค๊ฐ ์๋น
โ
๊ตฌ๋
์กฐ๊ฑด(๊ธฐ๊ด, ๋ถ์) ์ฒดํฌ
โ
์กฐ๊ฑด ๋ง์กฑ ์ ๋ฉ์์ง ๋ฐ์ก
๋ฉ์์ง ํ์:
๐ข [๊ธ์ต์์ํ] ๋ณด๋์๋ฃ
์ ๋ชฉ: ๋ง์ด๋ฐ์ดํฐ ํ์ฉ ๊ธ๋ฆฌ์ธํ์๊ตฌ ์๋น์ค ํ์ ๊ธ์ต 3๊ฑด ์ง์
๋งค์นญ๋ถ์: ๋์งํธ๊ธ์ต์ด๊ด๊ณผ (87%)
๊ฒ์: 2026-06-17 14:30
๐ https://fsc.go.kr/...
Bot ๋ช ๋ น์ด
/start - ๊ตฌ๋
์์ ์๋ด
/subscribe - ๊ตฌ๋
์กฐ๊ฑด ์ค์ (๊ธฐ๊ด, ๋ถ์)
/unsubscribe - ๊ตฌ๋
ํด์
/status - ํ์ฌ ๊ตฌ๋
์ํ ํ์ธ
/latest - ์ต์ 5๊ฑด ์กฐํ
์์ ํญ๋ชฉ
[ ] python-telegram-bot ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ค์
[ ] Bot ์์ปค (telegram_worker.py) ๊ตฌํ
[ ] ๊ตฌ๋
์ค์ ํ
์ด๋ธ (telegram_subscriptions) ์ถ๊ฐ
[ ] /subscribe ๋ช
๋ น์ด + ์กฐ๊ฑด ์ ์ฅ ๋ก์ง
[ ] ์ ๋ขฐ๋ < 40% ๊ฑด ์๋ฆผ ์ ์ธ ์ต์
[ ] ๋ฐ์ก ์คํจ ์ ์ฌ์๋ ๋ก์ง (์ต๋ 3ํ)
[ ] ํ๋ก ํธ์๋์์ Telegram ๊ตฌ๋
๋งํฌ ๋
ธ์ถ
Step 4 ์๋ฃ ๊ธฐ์ค (Definition of Done)
- ๊ณต์ ์นด๋ + ๋ถ์ ๋งค์นญ ์ ๋ขฐ๋ ๋ฐ ์ ์ ๋ ธ์ถ ํ์ธ
- ๊ธฐ๊ด/๊ธฐ๊ฐ/๋ถ์ ํํฐ ์ ์ ๋์ ํ์ธ
- ๊ณต์ ์์ธ ํ์ด์ง ๋ณธ๋ฌธ + Top-5 ํ๋ณด ํ์ ํ์ธ
- ๋ชจ๋ฐ์ผ ๋ฐ์ํ ๋ ์ด์์ ํ์ธ (iPhone 14 ๊ธฐ์ค)
- Telegram Bot ์ ๊ท ๊ณต์ ์๋ฆผ ๋ฐ์ก ํ์ธ
- Lighthouse ์ฑ๋ฅ ์ ์ โฅ 85
Step 5. ํตํฉ ํ ์คํธ ๋ฐ ์ด์ ์์ ํ (Week 10~13)
๋ชฉํ
์ ์ฒด ์์คํ E2E ๊ฒ์ฆ + ๋ชจ๋ธ ์ฑ๋ฅ ์ธก์ + ์ด์ ๋ชจ๋ํฐ๋ง ์ฒด๊ณ ๊ตฌ์ถ
5-1. E2E ํตํฉ ํ ์คํธ (Week 10~11)
ํ ์คํธ ์๋๋ฆฌ์ค
์๋๋ฆฌ์ค 1: ์ ์ ์์ง ํ๋ก์ฐ
1. ํฌ๋กค๋ฌ ์๋ ์คํ
2. ์ ๊ท ๊ณต์ DB ์ ์ฌ ํ์ธ
3. ๋งค์นญ ์์ปค ์๋ ์คํ
4. dept_matches ์ ์ฌ ํ์ธ
5. API ๋ชฉ๋ก ์กฐํ โ ๊ณต์ + ๋ถ์ ๋
ธ์ถ ํ์ธ
6. Telegram ์๋ฆผ ์์ ํ์ธ
์๋๋ฆฌ์ค 2: ์ค๋ณต ์์ง ๋ฐฉ์ง
1. ๊ฐ์ ๊ณต์ 2ํ ํฌ๋กค๋ง
2. DB ์ค๋ณต ์ ์ฌ 0๊ฑด ํ์ธ
์๋๋ฆฌ์ค 3: ์ ๋ขฐ๋ ๋ฎ์ ๊ฑด ์ฒ๋ฆฌ
1. ๊ด๋ จ์ฑ ๋ฎ์ ๋ณธ๋ฌธ ๋งค์นญ ์์ฒญ
2. ์ ๋ขฐ๋ < 0.4 โ needs_manual_review: true ํ์ธ
3. review-queue API์์ ํด๋น ๊ฑด ๋
ธ์ถ ํ์ธ
์๋๋ฆฌ์ค 4: ํฌ๋กค๋ฌ ์ฅ์ ๋ณต๊ตฌ
1. ๊ธฐ๊ด ์ฌ์ดํธ ์ ๊ทผ ๋ถ๊ฐ ์๋ฎฌ๋ ์ด์
2. ์๋ฌ ๋ก๊ทธ ๊ธฐ๋ก ํ์ธ
3. ๋ค์ ์ค์ผ์ค ์ฃผ๊ธฐ์ ์๋ ์ฌ์๋ ํ์ธ
์๋๋ฆฌ์ค 5: ์๋ ๋งค์นญ ์ฌ์์ฒญ
1. ์๋ชป ๋งค์นญ๋ ๊ณต์ ์ ํ
2. /rematch API ํธ์ถ (override_dept ์ง์ )
3. dept_matches ๊ฐฑ์ ํ์ธ
4. ํ๋ก ํธ์๋ ๊ฐฑ์ ํ์ ํ์ธ
์์ ํญ๋ชฉ
[ ] pytest E2E ํ
์คํธ ์ค์ํธ ์์ฑ
[ ] ๊ฐ ์๋๋ฆฌ์ค ์๋ํ ํ
์คํธ ๊ตฌํ
[ ] ๋ถํ ํ
์คํธ: API ๋์ 50 ์์ฒญ โ p95 ์๋ต โค 500ms
[ ] ํฌ๋กค๋ฌ 72์๊ฐ ์ฐ์ ์คํ ์์ ์ฑ ํ
์คํธ
5-2. ๋ชจ๋ธ ์ฑ๋ฅ ํ๊ฐ ๋ฐ ๊ฐ์ (Week 11~12)
ํ๊ฐ ๋ฐ์ดํฐ์ ๊ตฌ์ฑ
- ์๋ ๋ ์ด๋ธ๋ง: ์ค์ ๊ณต์ 50๊ฑด์ ๋ํ ๋ด๋น ๋ถ์ ์ ๋ต ๋ ์ด๋ธ
- ํ๊ฐ ์งํ: Top-1 ์ ํ๋, Top-5 ์ ํ๋, ํ๊ท ์ ๋ขฐ๋ ์ ์
- ๋ชฉํ: Top-1 โฅ 80%, Top-5 โฅ 95%
๋ชจ๋ธ ๊ฐ์ ํฌ์ธํธ (๋ชฉํ ๋ฏธ๋ฌ ์)
A. ๋ณธ๋ฌธ ์ ์ฒ๋ฆฌ ๊ฐ์
- ๊ณต์ ์ 512 ํ ํฐ โ ์ ๋ชฉ + ์ 300ํ ํฐ ํผํฉ ๋ฐฉ์ ์๋
- ์๋ฌธ ๊ธฐ์ฌ ๋ด๋น๋ถ์ ํ
์คํธ๋ฅผ ์ถ๊ฐ ํํธ๋ก ํ์ฉ
B. ์์๋ธ ๊ฐ์ค์น ํ๋
- dense_weight / tfidf_weight / bm25_weight ๊ทธ๋ฆฌ๋ ์์น
- ํ์ฌ ๊ธฐ๋ณธ๊ฐ โ ์ต์ ๊ฐ ํ์
C. Stage 2 ํ์ต ๋ฐ์ดํฐ ๋ณด๊ฐ
- ์ค์ ๊ณต์-๋ถ์ ๋งคํ ๋ ์ด๋ธ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐ ํ์ต์์ผ๋ก ํ์ฉ
- Negative ์ํ ์ ๋ต ๊ฐ์
๋ชจ๋ธ ์ฌํ์ต ํ์ดํ๋ผ์ธ ์๋ํ
dept.xlsx ์
๋ฐ์ดํธ ๊ฐ์ง
โ
Stage 2 ์ฌํ์ต ์๋ ํธ๋ฆฌ๊ฑฐ (์ฃผ 1ํ ๋ฐฐ์น)
โ
์ ๊ท ๋ชจ๋ธ ํ๊ฐ (์ฑ๋ฅ ํ๊ท ์๋์ง ํ์ธ)
โ
symlink ๊ต์ฒด๋ก ๋ฌด์ค๋จ ๋ชจ๋ธ ์
๋ฐ์ดํธ
โ
model_version ์
๋ฐ์ดํธ ๊ธฐ๋ก
์์ ํญ๋ชฉ
[ ] ํ๊ฐ ๋ฐ์ดํฐ์
50๊ฑด ์๋ ๋ ์ด๋ธ๋ง
[ ] ๋ชจ๋ธ ์ฑ๋ฅ ํ๊ฐ ์คํฌ๋ฆฝํธ ์์ฑ
[ ] Top-1 / Top-5 ์ ํ๋ ์ธก์ ๋ฐ ๊ฒฐ๊ณผ ๋ฌธ์ํ
[ ] ๋ฏธ๋ฌ ์ ๊ฐ์ ์ A/B/C ์ค ์ ์ฉ
[ ] dept.xlsx ๋ณ๊ฒฝ ๊ฐ์ง + ์ฌํ์ต ๋ฐฐ์น ์คํฌ๋ฆฝํธ
[ ] ๋ชจ๋ธ ๋ฒ์ ๊ด๋ฆฌ ์ฒด๊ณ ์๋ฆฝ
5-3. ์ด์ ๋ชจ๋ํฐ๋ง ์ฒด๊ณ ๊ตฌ์ถ (Week 12~13)
๋ชจ๋ํฐ๋ง ๋์๋ณด๋ (Grafana ๋๋ ๋ด์ฅ)
ํจ๋ 1: ๊ธฐ๊ด๋ณ ์๊ฐ๋น ์์ง ๊ฑด์ (์๊ณ์ด ๊ทธ๋ํ)
ํจ๋ 2: ํฌ๋กค๋ง ์ฑ๊ณต๋ฅ (๋ชฉํ: โฅ 99%)
ํจ๋ 3: ๋งค์นญ ์ ๋ขฐ๋ ๋ถํฌ ํ์คํ ๊ทธ๋จ
ํจ๋ 4: ์๋ ๊ฒํ ํ ๋์ ๊ฑด์
ํจ๋ 5: API ์๋ต ์๊ฐ p50/p95
ํจ๋ 6: ๋ชจ๋ธ ์๋ฒ ๋ฉ๋ชจ๋ฆฌ/CPU ์ฌ์ฉ๋ฅ
์๋ฆผ ์ค์
- ํฌ๋กค๋ฌ ์ฐ์ 3ํ ์คํจ โ ์ฆ์ ์๋ฆผ (Slack/์ด๋ฉ์ผ)
- ๋งค์นญ ์์ปค 5๋ถ ์ด์ ์ค๋จ โ ์ฆ์ ์๋ฆผ
- ์๋ ๊ฒํ ํ 50๊ฑด ์ด๊ณผ โ ๊ฒฝ๊ณ ์๋ฆผ
- API ์ค๋ฅ์จ > 1% โ ๊ฒฝ๊ณ ์๋ฆผ
๋ฐฐํฌ ๊ตฌ์ฑ (Docker Compose โ ์ด์ ํ๊ฒฝ)
services:
postgres: # DB
redis: # ๋ฉ์์ง ํ + ์บ์
crawler: # ํฌ๋กค๋ฌ + ์ค์ผ์ค๋ฌ
matcher: # ๋ชจ๋ธ ์๋ฒ (FastAPI)
worker: # ๋งค์นญ ์์ปค + Telegram ์์ปค
api: # REST API ์๋ฒ (FastAPI)
frontend: # Next.js (๋๋ Vercel ๋ฐฐํฌ)
nginx: # ๋ฆฌ๋ฒ์ค ํ๋ก์ + HTTPS
์ด์ ๋ฌธ์ ์์ฑ
[ ] ์๋น์ค ์ํคํ
์ฒ ๋ค์ด์ด๊ทธ๋จ
[ ] ํฌ๋กค๋ฌ ์ถ๊ฐ ๊ฐ์ด๋ (์ ๊ท ๊ธฐ๊ด ์จ๋ณด๋ฉ)
[ ] ๋ชจ๋ธ ์ฌํ์ต ์ด์ ์ ์ฐจ์
[ ] ์ฅ์ ๋์ ๋ฐ๋ถ (Runbook)
[ ] API ์ฌ์ฉ ๊ฐ์ด๋ (๋ด๋ถ ๋ฐฐํฌ์ฉ)
์์ ํญ๋ชฉ
[ ] Prometheus + Grafana ๋๋ ๋ด์ฅ ์งํ ์์ง ์ค์
[ ] ํฌ๋กค๋ฌ/์์ปค/API ํต์ฌ ์งํ ๊ณ์ธก ์ฝ๋ ์ถ๊ฐ
[ ] ์๋ฆผ ์๊ณ๊ฐ ์ค์ ๋ฐ ์์ ์ฑ๋ ์ฐ๊ฒฐ
[ ] Docker Compose ์ด์ ๋ฐฐํฌ ํ์ผ ์ต์ข
ํ
[ ] ์ ์ฒด ์ด์ ๋ฌธ์ ์์ฑ
[ ] ํ์ผ๋ฟ ์ด์ (๋ด๋ถ ์ฌ์ฉ์ 3~5๋ช
๋์, 2์ฃผ)
Step 5 ์๋ฃ ๊ธฐ์ค (Definition of Done)
- E2E ์๋๋ฆฌ์ค 5๊ฐ ์ ๋ถ ํต๊ณผ
- Top-1 ์ ํ๋ โฅ 80% ๋ฌ์ฑ ํ์ธ
- ๋ชจ๋ํฐ๋ง ๋์๋ณด๋ ํต์ฌ ์งํ 6๊ฐ ์ ์ ํ์
- ํฌ๋กค๋ฌ 72์๊ฐ ๋ฌด์ค๋จ ์์ ์ด์ ํ์ธ
- ์ด์ ๋ฌธ์ 4์ข ์์ฑ
- ๋ด๋ถ ํ์ผ๋ฟ ์ฌ์ฉ์ ํผ๋๋ฐฑ ์๋ ด ๋ฐ ์ฐ์ ์์ ๋ฒ๊ทธ ์์
๊ธฐ์ ์คํ ์์ฝ
| ์์ญ | ๊ธฐ์ |
|---|---|
| ํฌ๋กค๋ฌ | Python, requests, BeautifulSoup4, Playwright (JS๋ ๋๋ง fallback) |
| ์ค์ผ์ค๋ฌ | APScheduler |
| ๋ฉ์์ง ํ | Redis Stream |
| DB | PostgreSQL 16 + SQLAlchemy + Alembic |
| ๋ชจ๋ธ | SentenceTransformer (jhgan/ko-sroberta-multitask), TF-IDF, BM25 |
| ๋ชจ๋ธ ์๋ฒ | FastAPI + uvicorn |
| API ์๋ฒ | FastAPI + uvicorn + slowapi |
| ํ๋ก ํธ์๋ | Next.js 14, Tailwind CSS, shadcn/ui, TanStack Query |
| ์๋ฆผ | python-telegram-bot |
| ์ปจํ ์ด๋ | Docker + Docker Compose |
| ๋ฆฌ๋ฒ์ค ํ๋ก์ | Nginx |
| ๋ชจ๋ํฐ๋ง | Prometheus + Grafana (๋๋ ๋ด์ฅ ์งํ API) |
๋ฆฌ์คํฌ ๋ฐ ์์กด์ฑ
| ๋ฆฌ์คํฌ | ๋ฐ์ ๋จ๊ณ | ๋์ |
|---|---|---|
| ๊ธฐ๊ด ์ฌ์ดํธ JS ๋ ๋๋ง (ํฌ๋กค๋ฌ ํ์ฑ ์คํจ) | Step 1 | Playwright fallback ๊ตฌํ |
| GPU ๋ฏธํ๋ณด๋ก ๋ชจ๋ธ ์ฌํ์ต ์ง์ฐ | Step 2 | Google Colab ์ฆ์ ์ฌ์ฉ ๊ฐ๋ฅ, ์ฌ๋ด GPU ๋ณํ ํ๋ณด |
| Top-1 ์ ํ๋ 80% ๋ฏธ๋ฌ | Step 5 | ํ๊ฐ ํ ๊ฐ์ ์ A/B/C ์์ฐจ ์ ์ฉ, ์ผ์ 1์ฃผ ์๋น |
| dept.xlsx ๋ถ์ ์ ๋ณด ๋ฏธ๊ฐฑ์ | ์ด์ ์ ๋ฐ | Step 5์์ ์๋ํ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ์ผ๋ก ํด์ |
| ๊ธฐ๊ด ์ฌ์ดํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ | ์ด์ ์ ๋ฐ | ๊ธฐ๊ด๋ณ ํฌ๋กค๋ฌ ๋ ๋ฆฝ ๋ชจ๋ + ์คํจ ์๋ฆผ์ผ๋ก ๋น ๋ฅธ ์์ |