From 93b484a89cc40fc47c09f33bdc11fd8af44b404e Mon Sep 17 00:00:00 2001 From: Claude-51124 Date: Fri, 2 Jan 2026 12:28:26 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20DB=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20=EA=B4=80=EB=A6=AC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 계획 문서 archive로 이동 (구현 완료) - 트러블슈팅 문서 작성 (Phase 1-5 완료, 교훈 정리) - 기존 트러블슈팅 문서 링크 업데이트 --- .../plans/260102_db_scheduler_management.md | 154 ------------------ .../archive/260102_db_scheduler_management.md | 50 ++++++ ...rworks_slack_05_db_scheduler_management.md | 3 +- .../260102_db_scheduler_management.md | 70 ++++++++ 4 files changed, 122 insertions(+), 155 deletions(-) delete mode 100644 journey/plans/260102_db_scheduler_management.md create mode 100644 journey/plans/archive/260102_db_scheduler_management.md create mode 100644 journey/troubleshooting/260102_db_scheduler_management.md diff --git a/journey/plans/260102_db_scheduler_management.md b/journey/plans/260102_db_scheduler_management.md deleted file mode 100644 index 0fec9a7..0000000 --- a/journey/plans/260102_db_scheduler_management.md +++ /dev/null @@ -1,154 +0,0 @@ -# DB 기반 동적 스케줄러 관리 시스템 - -**날짜**: 2026-01-02 -**작성자**: happybell80 -**관련 서비스**: rb8001 -**상태**: 계획 - ---- - -## 목적 -main.py 732줄 중 130줄이 스케줄 관련 코드. DB로 이동하여 동적 관리. - -## 현재 스케줄러 현황 - -| 스케줄러 | 위치 | 환경변수 | 기본값 | 상태 | -|---------|------|---------|--------|------| -| naverworks_briefing | `app/scheduler/jobs/naverworks_briefing.py` | `NAVERWORKS_BRIEFING_ENABLED` | false | 환경변수 기반 | -| coldmail_briefing | `app/scheduler/jobs/coldmail_briefing.py` | `COLDMAIL_BRIEFING_ENABLED` | false | 환경변수 기반 | -| lunch_worldcup | `app/scheduler/jobs/lunch_worldcup.py` | `LUNCH_WORLDCUP_ENABLED` | **false** | **12시 스케줄, 비활성화 예정** | -| diary_generator | `app/scheduler/jobs/diary_generator.py` | `DIARY_GENERATOR_ENABLED` | true | 환경변수 기반 | -| daily_headlines | `main.py:164-205` | `HEADLINES_SCHEDULE_ENABLED` | true | main.py 직접 구현 | -| companyx_news | `main.py:208-248` | `COMPANY_X_NEWS_ENABLED` | true | main.py 직접 구현 | -| scheduler_status_check | `main.py:146-162` | 없음 | 항상 활성 | main.py 직접 구현 | - -## 테이블 구조 - -```sql -CREATE TABLE IF NOT EXISTS scheduled_jobs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(100) UNIQUE NOT NULL, - job_type VARCHAR(50) NOT NULL, - cron_expression VARCHAR(50) NOT NULL, - enabled BOOLEAN DEFAULT true, - config JSONB DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_enabled ON scheduled_jobs(enabled); -CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_job_type ON scheduled_jobs(job_type); -``` - -## Job Type 매핑 - -| job_type | 함수 경로 | 비고 | -|----------|----------|------| -| `naverworks_briefing` | `app.services.skills.naverworks_briefing.NanerWorksBriefingSkill.process_briefing` | async 함수 | -| `coldmail_briefing` | `app.scheduler.jobs.coldmail_briefing._run_coldmail_briefing` | async 함수 | -| `diary_generator` | `app.scheduler.jobs.diary_generator._run_diary_generator_with_logging` | sync 래퍼 | -| `lunch_worldcup` | `app.notifications.send_lunch_worldcup_notification` | async 함수 | -| `companyx_news` | `app.services.skills.news_posting_skill.NewsPostingSkill.process_news_batch` | async 함수 | -| `daily_headlines` | `app.services.skills.startup_news_skill.run_headlines_job` | async 함수 | - -## 마이그레이션 단계 - -### Phase 1: 테이블 및 Repository 생성 -- `app/state/scheduler_repository.py` 생성 -- `_ensure_table()` 함수로 테이블 생성 (다른 repository 패턴 참고) -- CRUD 함수 구현: `get_enabled_jobs()`, `create_job()`, `update_job()`, `delete_job()` - -### Phase 2: 12시 스케줄 비활성화 (즉시) -- `lunch_worldcup.py:16` 기본값 확인: 이미 `"false"`이므로 실행 안됨 -- DB 마이그레이션 전 명시적 비활성화: 환경변수 설정 또는 코드에서 주석 처리 - -### Phase 3: DB 기반 스케줄러 로더 구현 -- `app/scheduler/db_loader.py` 생성 -- `load_jobs_from_db(scheduler)` 함수: DB 조회 → job_type 매핑 → APScheduler 등록 -- async 함수 처리: `asyncio.run()` 또는 래퍼 함수 사용 - -### Phase 4: main.py 리팩토링 -- 기존 환경변수 기반 등록 코드 제거 (131-144줄) -- `db_loader.load_jobs_from_db(scheduler)` 호출로 대체 -- 헤드라인/뉴스 스케줄도 DB 기반으로 전환 - -### Phase 5: API 엔드포인트 구현 -- `app/router/scheduler_endpoint.py` 생성 (기존 `schedule_endpoint.py`와 분리) -- `POST /api/scheduler/jobs` - 새 잡 추가 -- `PATCH /api/scheduler/jobs/{name}` - 스케줄 수정 (enabled, cron_expression, config) -- `DELETE /api/scheduler/jobs/{name}` - 잡 삭제 -- `GET /api/scheduler/jobs` - 목록 조회 - -### Phase 6: 기존 데이터 마이그레이션 -- 환경변수 기반 스케줄러를 DB에 초기 데이터로 삽입 -- 마이그레이션 스크립트: `scripts/migrate_schedules_to_db.py` - -### Phase 7: 환경변수 관리 원칙 준수 (선택, 권장) -- `os.getenv()` 직접 호출 제거: `app/scheduler/jobs/*.py`에서 DB 조회로 대체 -- Pydantic Settings 전환: `app/core/config.py`에 스케줄러 관련 설정 추가 (하위 호환성 유지) -- `.env` 파일 정리: 스케줄러 관련 환경변수 제거 또는 주석 처리 -- **참고**: `311_FastAPI_구조_원칙.md:241-252` 환경변수 관리 원칙 - -## 동작 방식 -1. 서버 시작 시 `startup_event()`에서 `db_loader.load_jobs_from_db(scheduler)` 호출 -2. DB에서 `enabled=true` 조회 -3. `job_type`으로 함수 매핑 (매핑 테이블 사용) -4. APScheduler에 등록 -5. 런타임 중 API로 추가/수정/삭제 시 즉시 반영 (scheduler.add_job/remove_job) - -## API 엔드포인트 상세 - -### POST /api/scheduler/jobs -```json -{ - "name": "lunch_worldcup", - "job_type": "lunch_worldcup", - "cron_expression": "0 12 * * mon-fri", - "enabled": false, - "config": {"channel_id": "C09CP4MDX71"} -} -``` - -### PATCH /api/scheduler/jobs/{name} -```json -{ - "enabled": true, - "cron_expression": "0 13 * * mon-fri", - "config": {"channel_id": "C09CP4MDX71"} -} -``` - -## 초기 데이터 (마이그레이션용) - -| name | job_type | cron_expression | enabled | config | -|------|----------|-----------------|---------|--------| -| naverworks_daily | naverworks_briefing | 0 9 * * mon-fri | false | {} | -| coldmail_daily | coldmail_briefing | 5 9 * * mon-fri | false | {} | -| lunch_worldcup | lunch_worldcup | 0 12 * * mon-fri | **false** | {"channel_id": "C09CP4MDX71"} | -| daily_diary | diary_generator | 0 2 * * * | true | {} | -| daily_headlines | daily_headlines | 0 9 * * * | true | {"channel_id": "환경변수에서"} | -| companyx_news | companyx_news | 0 10 * * mon-fri | true | {"channel_id": "C09CP4MDX71"} | - -## 환경변수 하드코딩 문제 해결 - -### 현재 문제점 -- **이중 정의**: 코드 기본값(`os.getenv("LUNCH_WORLDCUP_ENABLED", "false")`)과 `.env` 파일 하드코딩(`LUNCH_WORLDCUP_ENABLED=true`) 불일치 -- **원칙 위반**: `311_FastAPI_구조_원칙.md:241-252`의 "단일 소스 원칙" 위반 - 코드에서 `os.getenv()` 직접 호출, `.env`에 하드코딩 -- **설정 관리 체계 부재**: Pydantic Settings 미사용으로 환경변수 관리 일관성 없음 - -### DB 마이그레이션으로의 해결 -- **스케줄러 관련 환경변수 하드코딩 문제 해결**: DB에서 `enabled` 컬럼으로 관리하여 `.env` 하드코딩 제거 가능 -- **코드 개선**: `os.getenv()` 직접 호출 제거, DB 조회로 대체 -- **부분적 해결**: 스케줄러 관련 문제만 해결, 전체 환경변수 관리 체계 개선은 별도 작업 필요 - -### 마이그레이션 시 주의사항 -- 기존 `.env` 파일의 스케줄러 관련 환경변수는 DB 초기 데이터로 마이그레이션 후 제거 -- 코드에서 `os.getenv()` 직접 호출하는 부분을 DB 조회로 변경 -- 하위 호환성을 위해 환경변수 체크 로직은 유지하되 DB 우선 적용 - -## 주의사항 -- `scheduler_status_check`는 시스템 내부용이므로 DB 관리 대상 아님 -- async 함수는 래퍼 함수 필요 (`_run_*_with_logging` 패턴) -- cron 표현식 파싱 로직 재사용 (`_parse_cron()` 함수) -- 기존 환경변수는 하위 호환성을 위해 유지하되 DB 우선 - diff --git a/journey/plans/archive/260102_db_scheduler_management.md b/journey/plans/archive/260102_db_scheduler_management.md new file mode 100644 index 0000000..4c686b3 --- /dev/null +++ b/journey/plans/archive/260102_db_scheduler_management.md @@ -0,0 +1,50 @@ +# DB 기반 동적 스케줄러 관리 시스템 + +**날짜**: 2026-01-02 +**작성자**: happybell80 +**관련 서비스**: rb8001 +**상태**: 구현 완료 (Phase 1-5) + +→ 상세: [troubleshooting/260102_db_scheduler_management.md](../../troubleshooting/260102_db_scheduler_management.md) + +--- + +## 목적 +main.py 732줄 중 130줄이 스케줄 관련 코드. DB로 이동하여 동적 관리. + +## 테이블 구조 + +```sql +CREATE TABLE IF NOT EXISTS scheduled_jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) UNIQUE NOT NULL, + job_type VARCHAR(50) NOT NULL, + cron_expression VARCHAR(50) NOT NULL, + enabled BOOLEAN DEFAULT true, + config JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +## Job Type 매핑 + +| job_type | 함수 경로 | 비고 | +|----------|----------|------| +| `lunch_worldcup` | `app.scheduler.jobs.lunch_worldcup._run_worldcup_with_logging` | sync 래퍼 | +| `diary_generator` | `app.scheduler.jobs.diary_generator._run_diary_generator_with_logging` | sync 래퍼 | +| `naverworks_briefing` | `app.scheduler.jobs.naverworks_briefing._run_briefing_with_logging` | sync 래퍼 | +| `coldmail_briefing` | `app.scheduler.jobs.coldmail_briefing._run_coldmail_briefing_with_logging` | sync 래퍼 | + +## 구현 완료 (Phase 1-5) + +- Phase 1-2: `app/state/scheduler_repository.py` - CRUD 함수 +- Phase 3: `app/scheduler/db_loader.py` - DB 로더 +- Phase 4: `main.py:146-149` - DB 기반 로드로 전환 +- Phase 5: `app/router/scheduler_endpoint.py` - API 엔드포인트 + +## 미완료 (Phase 6-7) + +- Phase 6: 기존 데이터 마이그레이션 스크립트 +- Phase 7: Pydantic Settings 전환 (선택, 권장) + diff --git a/journey/troubleshooting/250919_naverworks_slack_05_db_scheduler_management.md b/journey/troubleshooting/250919_naverworks_slack_05_db_scheduler_management.md index 1fa44e2..028a8fe 100644 --- a/journey/troubleshooting/250919_naverworks_slack_05_db_scheduler_management.md +++ b/journey/troubleshooting/250919_naverworks_slack_05_db_scheduler_management.md @@ -7,4 +7,5 @@ --- ## 계획 -→ 상세: [plans/260102_db_scheduler_management.md](../../plans/260102_db_scheduler_management.md) \ No newline at end of file +→ 상세: [plans/archive/260102_db_scheduler_management.md](../../plans/archive/260102_db_scheduler_management.md) +→ 구현: [260102_db_scheduler_management.md](260102_db_scheduler_management.md) \ No newline at end of file diff --git a/journey/troubleshooting/260102_db_scheduler_management.md b/journey/troubleshooting/260102_db_scheduler_management.md new file mode 100644 index 0000000..59a16f6 --- /dev/null +++ b/journey/troubleshooting/260102_db_scheduler_management.md @@ -0,0 +1,70 @@ +# DB 기반 스케줄러 관리 시스템 구현 + +**날짜**: 2026-01-02 +**작성자**: happybell80 +**관련 파일**: `rb8001/app/state/scheduler_repository.py`, `rb8001/app/scheduler/db_loader.py`, `rb8001/app/router/scheduler_endpoint.py`, `rb8001/main.py` + +--- + +## 문제 상황 + +### 환경변수 하드코딩 불일치 +- `lunch_worldcup.py:16`: 기본값 `"false"` 설정 +- `.env` 파일: `LUNCH_WORLDCUP_ENABLED=true` 하드코딩 +- 결과: 코드 기본값과 환경변수 불일치로 12시 스케줄 실행됨 +- 원인: `311_FastAPI_구조_원칙.md:241-252` 단일 소스 원칙 위반 + +### main.py 스케줄 코드 과다 +- 732줄 중 130줄이 스케줄 관련 코드 +- 환경변수 기반 등록으로 동적 관리 불가 + +## 해결 방안 + +### Phase 1-2: Repository 구현 +- `scheduler_repository.py`: `scheduled_jobs` 테이블 CRUD +- `_ensure_table()`: 테이블 자동 생성 +- JSONB `config` 필드 파싱: `json.loads()` 적용 (`get_enabled_jobs`, `get_job_by_name`) + +### Phase 3: DB 로더 구현 +- `db_loader.py`: `JOB_TYPE_MAP`으로 job_type → 함수 매핑 +- `load_jobs_from_db()`: DB 조회 → APScheduler 등록 +- async 함수는 sync 래퍼(`_run_*_with_logging`) 사용 + +### Phase 4: main.py 리팩토링 +- `main.py:146-149`: 환경변수 기반 등록 제거, `load_jobs_from_db()` 호출 +- `app.state.scheduler` 설정: `scheduler_endpoint.py`에서 `request.app.state.scheduler` 사용 + +### Phase 5: API 엔드포인트 +- `scheduler_endpoint.py`: CRUD + run 엔드포인트 +- `POST /api/scheduler/jobs`: 잡 생성 +- `PATCH /api/scheduler/jobs/{name}`: 수정 +- `DELETE /api/scheduler/jobs/{name}`: 삭제 +- `GET /api/scheduler/jobs`: 목록 조회 + +### 테스트 이슈 해결 +- JSONB 파싱: `asyncpg`가 JSONB를 string으로 반환 → `json.loads()` 적용 +- E2E 테스트 이벤트 루프 충돌: `pytest-asyncio`와 `TestClient` 충돌 +- 해결: 동기 `TestClient` fixture + `run_async()` 헬퍼 함수 사용 + +## 구현 완료 + +- 커밋: `1e82dee` (2026-01-02) +- 테스트: 13개 모두 통과 (repository 5, loader 3, E2E 5) +- 배포: Gitea Actions 자동 배포 완료 + +## 교훈 + +### 환경변수 관리 원칙 준수 +- `os.getenv()` 직접 호출 시 기본값과 `.env` 하드코딩 불일치 위험 +- DB 기반 설정으로 단일 소스 원칙 준수 가능 +- 향후 Pydantic Settings 전환 권장 (`Phase 7`) + +### JSONB 타입 처리 +- `asyncpg`는 JSONB를 string으로 반환하므로 명시적 파싱 필요 +- `json.loads()` 적용 위치: Repository 레이어에서 일관되게 처리 + +### E2E 테스트 이벤트 루프 관리 +- `pytest-asyncio`와 FastAPI `TestClient`의 lifespan 이벤트 충돌 가능 +- 해결: 동기 `TestClient` + 비동기 작업은 별도 루프에서 실행 +- `app.state` 패턴으로 의존성 주입 간소화 +