diff --git a/journey/plans/250906_news_system_integration.md b/journey/plans/250906_news_system_integration.md index 4ad1ba1..805dc9f 100644 --- a/journey/plans/250906_news_system_integration.md +++ b/journey/plans/250906_news_system_integration.md @@ -30,19 +30,102 @@ APScheduler → skill-news:8505 → rb8001 → Slack 채널 --- -## 3. 남은 작업 +## 3. 구현 완료 -### Phase 3: DB 영구 저장 +→ 상세: `troubleshooting/260127_news_history_api_implementation.md` -### 현재 제약 -- JSON 파일 (`/app/data/news_state.json`) 사용 -- 컨테이너 재시작 시 이력 손실 가능 -- 통계/분석 어려움 +--- -### 필요 작업 -1. `rb_news` 테이블 생성 (user_id, article_id, published_at, status) -2. news_posting_skill.py에 DB 저장 로직 추가 -3. 게시 이력 조회 API (`GET /api/news/history`) +## 4. 남은 작업 + +없음 (Phase 3 완료) + +**목표**: 사용자가 게시한 뉴스 이력을 조회할 수 있는 API 제공 + +**현재 상태**: +- `rb_news` 테이블 생성 완료 (troubleshooting/250920_news_db_persistence_implementation.md) +- DB 저장 로직 완료 (`news_posting_skill.py:save_news_to_db()`, `update_news_published_status()`) + +**기존 코드 수정 필요**: +- `rb_news` 테이블: `slack_user_id VARCHAR(100)` 컬럼 추가 (conversation_log 패턴과 일치, UUID 변환 실패 시 원본 보존) +- `news_posting_skill.py:update_news_published_status()`: `selected_by_user_id`(UUID), `selected_at`, `slack_user_id`(VARCHAR) 필드 업데이트 추가 +- `news_posting_skill.py:handle_publish_action()`: Slack ID → UUID 변환 로직 추가 (`get_user_uuid_by_slack_id()` 사용) + +**필요 작업** (계층 분리 원칙 준수): + +#### 3.1 DB 스키마 수정 +- `ALTER TABLE rb_news ADD COLUMN IF NOT EXISTS slack_user_id VARCHAR(100)` 실행 +- 목적: UUID 변환 실패 시 원본 Slack ID 보존 (conversation_log 패턴과 일치) +- 인덱스: `CREATE INDEX IF NOT EXISTS idx_rb_news_slack_user_id ON rb_news(slack_user_id)` (선택) + +#### 3.2 Repository 계층 (`app/state/repositories/news_repository.py`) +- 파일 신규 생성 +- `get_news_history()` 메서드: user_id, 기간, 상태 필터링, 페이지네이션 지원 +- 쿼리 조건: + - `selected_by_user_id = user_id` (필수, UUID로 조회, conversation_log 패턴과 일치) + - `is_published = TRUE` (기본, status 파라미터로 변경 가능) + - `published_at BETWEEN start_date AND end_date` (기간 필터, 선택) +- 정렬: `published_at DESC` (최신순) +- 페이지네이션: LIMIT/OFFSET 사용 +- 반환: Dict (items: List[Dict], total: int, page: int, limit: int, total_pages: int) +- 항목 필드: id, title, url, summary, published_at, is_published, squarespace_post_id, slack_channel_id, slack_user_id + +#### 3.2 Schema 계층 (`app/schemas/news_schema.py`) +- `app/schemas/` 폴더 신규 생성 (없는 경우) +- 파일 신규 생성 +- `NewsHistoryResponse`: 응답 스키마 (items, total, page, limit, total_pages) +- `NewsHistoryItem`: 개별 뉴스 항목 스키마 (id, title, url, summary, published_at, is_published, squarespace_post_id) + +#### 3.3 Service 계층 (`app/services/news_service.py`) +- 파일 신규 생성 +- `get_user_news_history()` 메서드: Repository 호출, 비즈니스 로직 처리 +- user_id 검증 (UUID 형식), 기본값 설정 (limit=20, page=1, max_limit=100) + +#### 3.4 Router 계층 (`app/router/news_endpoint.py`) +- 파일 신규 생성 +- `GET /api/news/history` 엔드포인트 +- 쿼리 파라미터: + - `page` (int, 기본값 1): 페이지 번호 + - `limit` (int, 기본값 20, 최대 100): 페이지 크기 + - `status` (str, 선택): 필터링 (published, pending 등) + - `start_date` (str, 선택): ISO 8601 형식 시작일 + - `end_date` (str, 선택): ISO 8601 형식 종료일 +- 인증: `Depends(get_current_user)` - current_user의 게시 이력만 조회 (user_id 파라미터 제거, 보안) +- Service 호출 → Schema로 응답 변환 +- 에러 처리: 400 (잘못된 파라미터), 401 (인증 실패), 500 (서버 오류) + +#### 3.5 Router 등록 (`main.py`) +- `news_endpoint.router` 등록 + +#### 3.6 기존 코드 수정 (`app/services/skills/news_posting_skill.py`) +- `handle_publish_action()` 메서드 수정: Slack ID → UUID 변환 로직 추가 + - `from app.state.repositories.user_repository import get_user_uuid_by_slack_id` import + - `user_id.startswith("U")` 체크 후 `get_user_uuid_by_slack_id(user_id)` 호출 + - 변환 성공 시 UUID 사용, 실패 시 원본 Slack ID 보존 +- `update_news_published_status()` 메서드 수정: user_id(UUID), slack_user_id(VARCHAR) 파라미터 추가 +- UPDATE 문에 `selected_by_user_id = user_id`, `selected_at = NOW()`, `slack_user_id = slack_user_id` 추가 +- 호출부 수정: `handle_publish_action()`에서 UUID와 원본 Slack ID 모두 전달 + +#### 3.7 테스트 +- `tests/test_news_history.py` 작성 +- 실제 DB 조회 테스트 (컨테이너 내부 실행) +- curl로 API 테스트: `GET /api/news/history?page=1&limit=20` + +**원칙 준수 체크리스트**: +- [ ] 계층 분리: router → services → state/repositories (311_백엔드_구조_원칙.md 섹션 1) +- [ ] Repository 패턴: DB 접근은 state/repositories에서만 (섹션 6) +- [ ] Schema 분리: API 요청/응답은 schemas로 (섹션 3) +- [ ] 파일 크기: 각 파일 500줄 이하 (섹션 7) +- [ ] 실제 테스트: 추측하지 말고 curl/DB 직접 확인 (섹션 18) +- [ ] 로깅: 시작/종료 INFO, 중간 과정 DEBUG (섹션 11) +- [ ] UUID 사용: user_id는 UUID string으로 처리 (AGENTS.md) + +**데이터 원칙**: +- [ ] 더미 데이터 금지: 실제 rb_news 테이블 데이터만 사용 (섹션 6) +- [ ] JSONB 처리: metadata 필드 사용 시 json.dumps() 필수 (섹션 6) +- [ ] DB 스키마 확인: rb_news 테이블 실제 구조 확인 후 구현 (섹션 6-1) +- [ ] 이중 저장 패턴: selected_by_user_id(UUID)와 slack_user_id(VARCHAR) 함께 저장 (conversation_log 패턴 준수) +- [ ] UUID 변환 실패 처리: 변환 실패 시 selected_by_user_id는 NULL, slack_user_id에 원본 저장 --- @@ -63,7 +146,18 @@ SKILL_PUBLISH_URL=http://localhost:8511 ## 5. 참고 문서 +### 구현 관련 - `troubleshooting/250907_company_x_news_zero_articles.md` - `troubleshooting/250908_skill_news_companyx_data_integration.md` - `troubleshooting/250908_slack_interactive_gateway_proxy.md` - `troubleshooting/250920_news_db_persistence_implementation.md` + +### 원칙 문서 +- `book/300_architecture/311_백엔드_구조_원칙.md` - 계층 분리, Repository 패턴, 테스트 원칙 +- `book/300_architecture/312_문서_작성_원칙.md` - 문서 작성 규칙 +- `book/300_architecture/database/tables.md` - rb_news 테이블 스키마 참고 + +### 참고 코드 +- `app/router/history_endpoint.py` - 페이지네이션 구현 예시 +- `app/state/repositories/user_repository.py` - Repository 패턴 예시 +- `app/services/skills/news_posting_skill.py:503-575` - 기존 DB 저장 로직 참고 diff --git a/journey/troubleshooting/260127_news_history_api_implementation.md b/journey/troubleshooting/260127_news_history_api_implementation.md new file mode 100644 index 0000000..afe70c6 --- /dev/null +++ b/journey/troubleshooting/260127_news_history_api_implementation.md @@ -0,0 +1,55 @@ +# 뉴스 게시 이력 조회 API 구현 완료 + +**날짜**: 2026-01-27 +**작성자**: happybell80 +**관련 파일**: +- `rb8001/app/router/news_endpoint.py` +- `rb8001/app/services/news_service.py` +- `rb8001/app/state/repositories/news_repository.py` +- `rb8001/app/schemas/news_schema.py` +- `rb8001/app/services/skills/news_posting_skill.py` + +--- + +## 문제 상황 + +- 사용자가 게시한 뉴스 이력을 조회할 수 있는 API 부재 +- `rb_news` 테이블에 `selected_by_user_id`, `slack_user_id` 컬럼 없음 +- UUID 변환 로직 없어 Slack ID가 그대로 저장됨 + +## 해결 방안 + +### 1. DB 스키마 수정 +- `migrate_rb_news_add_user_columns.sql`: `selected_by_user_id`, `selected_at`, `slack_user_id` 컬럼 추가 +- conversation_log 패턴 준수: UUID와 Slack ID 이중 저장 + +### 2. 기존 코드 수정 +- `news_posting_skill.py:handle_publish_action()`: Slack ID → UUID 변환 로직 추가 (`get_user_uuid_by_slack_id()` 사용) +- `news_posting_skill.py:update_news_published_status()`: `user_uuid`, `slack_user_id` 파라미터 추가, DB 업데이트 + +### 3. 계층별 구현 (TDD) +- Repository: `news_repository.py` - 페이지네이션, 필터링 지원 +- Schema: `news_schema.py` - `NewsHistoryResponse`, `NewsHistoryItem` +- Service: `news_service.py` - UUID 검증, 기본값 설정 +- Router: `news_endpoint.py` - `GET /api/news/history` 엔드포인트 + +## 구현 완료 + +- 커밋: `b54a577` (2026-01-27) +- API 테스트: 정상 동작 확인 +- DB 마이그레이션: 완료 + +## 교훈 + +### DB 스키마 확인 필수 +- 계획 문서의 스키마와 실제 DB 스키마 불일치 (`squarespace_post_id` 컬럼 없음) +- 구현 전 실제 DB 스키마 확인 후 쿼리 작성 필요 + +### conversation_log 패턴 준수 +- UUID 변환 실패 시에도 원본 보존을 위해 `slack_user_id` 컬럼 필수 +- 이중 저장 패턴으로 호환성 확보 + +### TDD 진행 +- 테스트 작성 → 구현 → 실제 테스트 순서로 진행하여 문제 조기 발견 + +---