feat: 정책 핀 sync·배치 가공·DB 적재 파이프라인 및 관리자 API 추가#118
Conversation
정책뉴스 수집→LLM 가공→DB INSERT 일괄 파이프라인(sync_pipeline)과 /policy-admin 엔드포인트(sync, transform-batch, import-batch, pipeline-status)를 추가한다. KST 기준 자동 sync 스케줄러, 카드뉴스 S3 업로드·CardnewsImageS3 적재, 중복 건 스킵·파이프라인 정리, Community/EventPin(policy_api_id) 모델 확장을 포함한다.
sync_pipeline 다배치 실행 시 import errors/items/pin_ids가 마지막 배치만 반영되던 문제를 누적하도록 수정하고, 스케줄러 간격 비교에 1시간 버퍼를 적용해 실행 시각 오차로 sync가 하루 밀리는 현상을 방지한다. 동작하지 않던 cardnews_image_urls fallback을 제거하고, 카드뉴스 handoff_path 절대 경로 처리를 보완한다.
|
/gemini review |
|
/gemini review |
/policy-admin 접근 제어와 정책 파이프라인 캡슐화를 보강하고, 카드뉴스 정리 및 nested transaction 실패 처리의 데이터 유실 위험을 줄인다. Co-authored-by: Cursor <cursoragent@cursor.com>
|
/gemini review |
/policy-admin 날짜 검증 예외가 400으로 처리되도록 보완하고, Community.pin_id nullable 변경으로 기존 데이터 마이그레이션 리스크를 줄인다.
|
/gemini review |
handoff 적재와 sync_pipeline의 중복 DB 조회를 줄이고, 카드뉴스 파일 읽기·파이프라인 정리 작업을 스레드 풀에서 실행해 이벤트 루프 블로킹을 방지한다.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces an automated background scheduler and admin API endpoints for syncing, transforming, and importing policy pins and their associated cardnews images to S3 and the database. Key additions include the PolicyPinSchedulerService, PolicyEventIngestService, and the CardnewsImageS3 model, along with comprehensive unit tests. Feedback on these changes highlights a potential runtime lookup error due to a mismatch between the configuration alias POLICY_ADMIN_NICKNAME and the database query field, a blocking synchronous file I/O operation in sync_pipeline that should be offloaded to a thread pool, and redundant slicing logic in import_handoff_batch that can be simplified.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| ) | ||
| policy_admin_user_name: str = Field( | ||
| default="admin", | ||
| validation_alias=AliasChoices("POLICY_ADMIN_USER_NAME", "POLICY_ADMIN_NICKNAME"), |
There was a problem hiding this comment.
설정 필드 policy_admin_user_name에서 POLICY_ADMIN_NICKNAME을 별칭(alias)으로 허용하고 있지만, PolicyEventIngestService.resolve_admin_uid에서는 이 값을 항상 user_name 컬럼 기준으로만 조회(get_by_user_name)하고 있습니다. 만약 사용자가 POLICY_ADMIN_NICKNAME 환경 변수를 설정할 경우, 실제 DB의 nickname 컬럼이 아닌 user_name 컬럼에서 조회되어 사용자를 찾지 못하는 런타임 오류가 발생할 수 있습니다. 별칭에서 POLICY_ADMIN_NICKNAME을 제거하거나, 조회 시 닉네임과 유저명을 모두 고려하도록 로직을 수정하는 것이 안전합니다.
| validation_alias=AliasChoices("POLICY_ADMIN_USER_NAME", "POLICY_ADMIN_NICKNAME"), | |
| validation_alias=AliasChoices("POLICY_ADMIN_USER_NAME"), |
|
|
||
| if settings.policy_prune_pipeline_after_import: | ||
| db_ids_before = await ingest_service.get_imported_policy_api_ids() | ||
| prune_pipeline_imported(db_ids_before) |
There was a problem hiding this comment.
동기식 파일 I/O를 수행하는 prune_pipeline_imported 함수를 비동기 이벤트 루프 내에서 직접 호출하고 있어, 파일 크기가 커질 경우 이벤트 루프가 블로킹될 위험이 있습니다. PolicyEventIngestService에서와 마찬가지로 anyio.to_thread.run_sync를 사용하여 별도 스레드 풀에서 실행하도록 개선하는 것을 권장합니다.
| prune_pipeline_imported(db_ids_before) | |
| from anyio import to_thread | |
| await to_thread.run_sync(prune_pipeline_imported, db_ids_before) |
| target_rows = pending_rows if import_all else pending_rows[: (limit or len(pending_rows))] | ||
| if limit is not None and import_all: | ||
| target_rows = pending_rows[:limit] |
There was a problem hiding this comment.
라인 152~154의 슬라이싱 로직은 다소 복잡하고 중복되어 있습니다. 특히 import_all이 False이고 limit가 None인 경우, pending_rows[:len(pending_rows)]가 되어 결국 전체 행을 가져오게 되므로 import_all=True일 때와 동일하게 동작합니다. 이는 import_all 매개변수의 의도와 다르게 동작할 수 있으므로, 로직을 더 명확하고 단순하게 개선하는 것이 좋습니다.
| target_rows = pending_rows if import_all else pending_rows[: (limit or len(pending_rows))] | |
| if limit is not None and import_all: | |
| target_rows = pending_rows[:limit] | |
| if limit is not None: | |
| target_rows = pending_rows[:limit] | |
| elif not import_all: | |
| target_rows = [] | |
| else: | |
| target_rows = pending_rows |
sync가 remaining_pending 기준으로 배치를 끝까지 돌고, import 후 캐시 정리로 handoff가 비어도 DB 적재 완료로 처리하도록 status·hint를 보강한다.
✨ 작업 개요
sync_pipeline: 수집·배치 가공·배치 DB 적재를 한 번에 실행/policy-admin엔드포인트:sync,transform-batch,import-batch,status,scheduler/run-oncePolicyPinSchedulerService) 앱 lifespan 등록CardnewsImageS3적재EventPin.policy_api_id,Community확장으로 중복 적재 방지체크리스트
📷 이미지 첨부 (선택)
POST /policy-admin/sync응답 샘플GET /policy-admin/status파이프라인 상태 응답🧐 집중 리뷰 요청
PolicyEventIngestService.import_handoff_batch— Pin / EventPin / Community / CardnewsImageS3 트랜잭션·중복 스킵 로직PolicyPinService.sync_pipeline— 수집→가공→적재 배치 루프 및policy_api_id기준 중복 처리PolicyPinSchedulerService— KST 스케줄 간격(POLICY_SYNC_INTERVAL_DAYS) 및 lifespan 등록/종료policy_pipeline_cleanup,POLICY_PRUNE_PIPELINE_AFTER_IMPORT)Community·EventPin모델 변경(policy_api_id,community_type등) — 기존 데이터·마이그레이션 영향