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 ์ธํฐํ์ด์ค | |
| ```python | |
| 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 | ๊ธํต์ ์๊ฒฐ ๋ณ๋ ์นดํ ๊ณ ๋ฆฌ | | |
| #### ๊ณตํต ์ฒ๋ฆฌ ๋ก์ง | |
| ```python | |
| # ์ค๋ณต ๋ฐฉ์ง: 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) | |
| ```python | |
| # 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) | |
| ``` | |
| #### ๋ชจ๋ธ ์ฑ๋ฅ ๊ธฐ์ค ๊ฒ์ฆ | |
| ```python | |
| # ํ ์คํธ ์ผ์ด์ค (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 ์๋ํฌ์ธํธ | |
| ```python | |
| # 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" | |
| } | |
| ``` | |
| #### ๋ชจ๋ธ ์๋ฒ ์ค๊ณ ํฌ์ธํธ | |
| ```python | |
| # ์ฑ ์์ ์ 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 ์ฟผ๋ฆฌ ์ต์ ํ | |
| ```sql | |
| -- ์์ฃผ ์ฌ์ฉ๋๋ ํํฐ ์กฐํฉ์ ์ธ๋ฑ์ค ์ถ๊ฐ | |
| 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 (๊ณต์ ์นด๋) | |
| ```tsx | |
| // ํ์ ์ ๋ณด | |
| // - ๊ธฐ๊ด ๋ฐฐ์ง (์์ ๊ตฌ๋ถ: ๊ธ์ต์=ํ๋, ๊ธ๊ฐ์=์ด๋ก, ...) | |
| // - ์นดํ ๊ณ ๋ฆฌ ํ๊ทธ | |
| // - ์ ๋ชฉ (2์ค ๋ง์ค์) | |
| // - ๊ฒ์์ผ์ (์๋์๊ฐ: "3์๊ฐ ์ ") | |
| // - ๋งค์นญ ๋ถ์ + ์ ๋ขฐ๋ ๋ฐ | |
| // - ํ๋ณด ๋ถ์ 2~3๊ฐ (์์ ๊ธ์จ) | |
| // - ์๋ ๊ฒํ ํ์ ๊ฒฝ๊ณ (์ ๋ขฐ๋ < 40%) | |
| // - [์๋ฌธ ๋ณด๊ธฐ] ๋ฒํผ | |
| ``` | |
| #### DeptMatchBadge (๋ถ์ ๋งค์นญ ์๊ฐํ) | |
| ```tsx | |
| // ๋์ (80%+): ์ด๋ก progress bar | |
| // ๋ณดํต (60~79%): ๋ ธ๋ progress bar | |
| // ๋ฎ์ (40~59%): ์ฃผํฉ progress bar | |
| // ๊ฒํ ํ์ (<40%): ๋นจ๊ฐ + "โ ๏ธ ์๋ ๊ฒํ " ํ ์คํธ | |
| ``` | |
| #### FilterBar (ํํฐ ์์ญ) | |
| ```tsx | |
| // - ๊ธฐ๊ด ํญ (์ ์ฒด/๊ธ์ต์/๊ธ๊ฐ์/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 โ ์ด์ ํ๊ฒฝ) | |
| ```yaml | |
| 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์์ ์๋ํ ํ์ดํ๋ผ์ธ ๊ตฌ์ถ์ผ๋ก ํด์ | | |
| | ๊ธฐ๊ด ์ฌ์ดํธ ๊ตฌ์กฐ ๋ณ๊ฒฝ | ์ด์ ์ ๋ฐ | ๊ธฐ๊ด๋ณ ํฌ๋กค๋ฌ ๋ ๋ฆฝ ๋ชจ๋ + ์คํจ ์๋ฆผ์ผ๋ก ๋น ๋ฅธ ์์ | | |