[FEAT] policy api 구현#86
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive policy cardnews rendering package (app/policy_cardnews/) and templates, along with corresponding API clients (PolicyNewsClient, VisitKoreaClient), routes, services, and schemas for managing both festival and policy pins. It also refactors vector store domain configurations and adjusts reliability pipeline timeouts. The reviewer feedback highlights several critical issues: a bug in slide merging that can corrupt cover slides, a slicing issue that discards the final CTA slide, significant code duplication between app/policy_cardnews/ and app/utils/, a missing fallback to VISITKOREA_SERVICE_KEY in PolicyNewsClient, an unused parameter in _render_slide_image, and broken encoding (Mojibake) in a log message.
There was a problem hiding this comment.
Pull request overview
정책브리핑(정책뉴스)·TourAPI 기반 콘텐츠를 수집/가공해 DB 핸드오프(JSONL)까지 이어지는 파이프라인을 추가하고, 정책 카드뉴스를 고정 템플릿 기반으로 렌더링(또는 이미지 모델 생성)할 수 있도록 확장합니다. 또한 민원(청원) 의견서 생성(VLM+RAG+PDF) 기능까지 함께 포함되어 서비스 범위가 크게 넓어졌습니다.
Changes:
- 정책 핀: 정책뉴스 OpenAPI 수집(
/policy-pins/search) → Gemini 가공(/policy-pins/transform) → JSONL 조회(/policy-pins/handoff) 파이프라인 및 DTO/문서 추가 - 축제 핀: TourAPI 기반 검색/가공/핸드오프 API와 Cron용 배치 스크립트 추가
- 카드뉴스/민원: 정책 카드뉴스 템플릿 렌더러(팔레트/레이아웃/용어 정리) 및 민원 의견서(VLM+RAG+rereank+PDF) 생성 기능과 관련 의존성/CI 설정 추가
Reviewed changes
Copilot reviewed 63 out of 70 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/preview_policy_cardnews.py | 정책 카드뉴스 렌더링 미리보기 스크립트 추가 |
| requirements.txt | PDF/문서 변환 및 카드뉴스/배치에 필요한 의존성 대폭 추가 |
| rag/scripts/transform_policy_content.py | 정책 문서 JSONL → Gemini 쉬운설명 변환 스크립트 추가 |
| rag/scripts/transform_festival_content.py | 축제 문서 JSONL → Gemini 인스타 톤 변환 스크립트 추가 |
| rag/scripts/run_festival_pipeline.py | Cron용 축제 fetch→transform 배치 파이프라인 추가 |
| rag/scripts/fetch_policy_news.py | 정책브리핑 정책뉴스 OpenAPI 수집 스크립트 추가 |
| docs/policy_pin_handoff.md | 정책 핀 JSONL 핸드오프/Swagger 사용법 문서화 |
| docs/festival_pin_handoff.md | 축제 핀 JSONL 핸드오프 및 Cron 운영 가이드 문서화 |
| app/utils/visitkorea_facilities.py | TourAPI intro/pet 응답에서 반려동물/숙박 정보 추출 유틸 추가 |
| app/utils/policy_cardnews_visual.py | (레거시 렌더러용) 정책 카드뉴스 비주얼 합성 유틸 추가 |
| app/utils/policy_cardnews_typography.py | (레거시 렌더러용) 텍스트 그리기 유틸 추가 |
| app/utils/policy_cardnews_template_metrics.py | 정책 템플릿 메트릭 상수 파일 추가 |
| app/utils/festival_date_filter.py | 축제 기간 겹침 필터/날짜 검증 유틸 추가 |
| app/services/VectorStoreService.py | 도메인별 테이블명 해석 로직 확장(standalone 테이블 옵션) |
| app/services/vector_domains.py | 벡터 도메인 설정 빌더/standalone_table 플래그 추가 |
| app/services/templates/complaint_opinion_form.html.j2 | 의견제출서 HTML 템플릿 추가 |
| app/services/RagRetrievalService.py | RAG retrieve + (옵션) rerank 파이프라인 서비스 추가 |
| app/services/RagRerankService.py | Gemini embedding 기반 코사인 유사도 rerank 서비스 추가 |
| app/services/prompts/vlm.py | VLM 프롬프트 모듈 export 정리(변경) |
| app/services/prompts/policy_pin.py | 정책 쉬운설명 생성 프롬프트 추가 |
| app/services/prompts/policy_cardnews.py | 정책 카드뉴스 슬라이드/이미지 프롬프트 추가 |
| app/services/prompts/festival_pin.py | 축제 인스타 스타일 본문 프롬프트 추가 |
| app/services/prompts/complaint_email_vlm.py | 민원 이미지+텍스트 분석 VLM 프롬프트/스키마 추가 |
| app/services/prompts/complaint_email_opinion.py | 의견제출서 섹션 JSON 생성 프롬프트/스키마 추가 |
| app/services/prompts/complaint_email_notification.py | 생성 결과 안내 메일 본문 포맷터 추가 |
| app/services/PolicyPinService.py | 정책 핀 search/transform/handoff 서비스 추가 |
| app/services/policy_pin_transform.py | 정책 문서 JSONL 변환 및 DB용 4필드 JSONL 생성 로직 추가 |
| app/services/policy_cardnews.py | 정책 카드뉴스 이미지 생성(이미지 모델/템플릿) 오케스트레이션 추가 |
| app/services/internal/ComplaintEmailPdfService.py | HTML→PDF 변환(WeasyPrint + Playwright fallback) 추가 |
| app/services/internal/ComplaintEmailOpinionRenderer.py | 의견서 섹션 JSON 파싱 및 HTML 렌더링/첨부이미지 처리 추가 |
| app/services/internal/ai/PolicyCardnewsImageService.py | Gemini/Imagen 기반 카드뉴스 이미지 생성 서비스 추가 |
| app/services/internal/ai/ComplaintEmailVLMService.py | 민원 VLM 호출/결과 정규화 서비스 추가 |
| app/services/internal/ai/ComplaintEmailLLMService.py | 의견서 섹션 JSON 생성 및 HTML 렌더링 호출 서비스 추가 |
| app/services/FestivalPinService.py | 축제 핀 search/transform/handoff 서비스 추가 |
| app/services/festival_pin_transform.py | 축제 문서 JSONL 변환 및 DB용 JSONL 생성 로직 추가 |
| app/services/ComplaintEmailVlmService.py | 내부 VLM 서비스 re-export(하위 호환) 추가 |
| app/services/ComplaintEmailService.py | 민원 패키지(VLM+RAG+LLM+PDF) 파이프라인 서비스 추가 |
| app/services/init.py | 서비스 export 확장(ComplaintEmail*, build_vector_domain_configs 등) |
| app/schemas/PolicyPinDTO.py | 정책 핀 소스/핸드오프/응답 DTO 추가 |
| app/schemas/FestivalPinDTO.py | 축제 핀 소스/핸드오프/응답 DTO 추가 |
| app/schemas/ComplaintEmailDTO.py | 민원 파이프라인 입력/출력 DTO 추가 |
| app/routes/PolicyPinRoute.py | 정책 핀 API 라우트 추가 |
| app/routes/FestivalPinRoute.py | 축제 핀 API 라우트 추가 |
| app/routes/ComplaintEmailRoute.py | 민원 의견서 생성 API 라우트 추가 |
| app/routes/init.py | 신규 라우터 등록 및 env별 활성화 설정 |
| app/policy_cardnews/visual.py | 템플릿 렌더링용 이미지 합성 유틸 추가 |
| app/policy_cardnews/terms.py | 정책 용어 쉬운말 치환/용어 가이드 생성 로직 추가 |
| app/policy_cardnews/template/metrics.py | 템플릿 전용 수치 상수 추가 |
| app/policy_cardnews/template/draw.py | 템플릿 공통 드로잉 유틸 추가 |
| app/policy_cardnews/template/init.py | 템플릿 디스패치/컨텍스트 API export 추가 |
| app/policy_cardnews/slides.py | 카드뉴스 슬라이드 JSON 파싱 및 레이아웃 자동 결정 추가 |
| app/policy_cardnews/render.py | 정책 카드뉴스 템플릿 렌더링 파이프라인 추가 |
| app/policy_cardnews/images.py | 원문/커버 이미지 다운로드 유틸 추가 |
| app/policy_cardnews/copy.py | 카드뉴스 카피 정리/병합/필러 제거 로직 추가 |
| app/policy_cardnews/constants.py | 카드뉴스 공통 상수 추가 |
| app/policy_cardnews/init.py | policy_cardnews 패키지 export 추가 |
| app/main.py | Vector domain config 빌더 적용 + Playwright 브라우저 라이프사이클 훅 추가 |
| app/core/deps.py | 신규 서비스 DI(축제/정책/민원/RAG) 추가 |
| app/core/config.py | TourAPI/정책뉴스/카드뉴스/민원 RAG/PDF 관련 설정 추가 |
| app/core/codes.py | 성공 코드(민원 패키지 생성) 추가 |
| app/clients/VisitKoreaClient.py | TourAPI HTTP 클라이언트 추가 |
| app/clients/PolicyNewsClient.py | 정책뉴스 OpenAPI HTTP 클라이언트 추가 |
| app/assets/policy_cardnews_templates/template_three_col.json | 카드뉴스 3열 템플릿 정의 추가 |
| app/assets/policy_cardnews_templates/template_numbered.json | 카드뉴스 번호형 템플릿 정의 추가 |
| app/assets/policy_cardnews_templates/template_grid.json | 카드뉴스 그리드 템플릿 정의 추가 |
| app/assets/policy_cardnews_templates/template_cta.json | 카드뉴스 CTA 템플릿 정의 추가 |
| app/assets/policy_cardnews_templates/template_cover.json | 카드뉴스 표지 템플릿 정의 추가 |
| app/assets/policy_cardnews_palettes.json | 템플릿 팔레트 정의 추가 |
| app/assets/mascots/mascots.json | 마스코트 파일 목록 추가/갱신 |
| .github/workflows/ci.yml | WeasyPrint 빌드용 시스템 라이브러리 설치 단계 추가 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from app.routes.ComplaintEmailRoute import router as complaint_email_router | ||
| from app.routes.ImageGeoRoute import router as image_geo_router | ||
| from app.routes.IssueRoute import router as issue_router | ||
| from app.routes.UserRoute import router as user_router | ||
| from app.routes.TestRoute import router as test_router | ||
| from app.routes.VectorTestRoute import router as vector_test_router | ||
| from app.routes.FestivalPinRoute import router as festival_pin_router | ||
| from app.routes.PolicyPinRoute import router as policy_pin_router | ||
| from app.core.config import settings | ||
|
|
||
| ROUTER_REGISTRY = ( | ||
| {"router": user_router, "disabled_envs": set()}, | ||
| {"router": issue_router, "disabled_envs": set()}, | ||
| {"router": complaint_email_router, "disabled_envs": set()}, | ||
| {"router": image_geo_router, "disabled_envs": set()}, | ||
| {"router": test_router, "disabled_envs": {"dev", "prod"}}, | ||
| {"router": vector_test_router, "disabled_envs": {"dev", "prod"}}, | ||
| {"router": festival_pin_router, "disabled_envs": {"prod"}}, | ||
| {"router": policy_pin_router, "disabled_envs": {"prod"}}, |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive pipeline for fetching, transforming, and rendering policy news and festival data into card news, including new API clients, Pillow-based rendering templates, FastAPI routes, and batch scripts. The review identified several critical issues and optimization opportunities: duplicate configuration fields in app/core/config.py, a logical bug in app/utils/visitkorea_facilities.py that misclassifies explicit 'false' values as missing information, and multiple performance bottlenecks such as uncached font loading, redundant mascot filtering, sequential HTTP requests during cover image enrichment, and repeated instantiation of httpx.AsyncClient inside a retry loop.
| rag_retrieve_top_k: int = Field(default=10, ge=1, le=100, alias="RAG_RETRIEVE_TOP_K") | ||
| rag_rerank_top_k: int = Field(default=5, ge=1, le=100, alias="RAG_RERANK_TOP_K") | ||
| rag_enable_rerank: bool = Field(default=False, alias="RAG_ENABLE_RERANK") | ||
| rag_vector_query_mode: str = Field(default="hybrid", alias="RAG_VECTOR_QUERY_MODE") |
| def _yn_to_label(value: object) -> str | None: | ||
| text = str(value or "").strip().upper() | ||
| if not text: | ||
| return None | ||
| if text in {"Y", "YES", "1", "가능", "O"}: | ||
| return "가능" | ||
| if text in {"N", "NO", "0", "불가", "X"}: | ||
| return "불가" | ||
| if len(text) <= 20: | ||
| return text | ||
| return None |
There was a problem hiding this comment.
value가 False일 때, value or "" 표현식은 빈 문자열 ""을 반환합니다. 이로 인해 text가 ""이 되어 if not text: 조건에 걸려 None을 반환하게 됩니다. 즉, 반려동물 동반이나 숙박 가능 여부가 명시적으로 "불가"(False)인 경우에도 "정보 없음"(None)으로 처리되는 심각한 논리 오류가 발생합니다.\n\nNone이나 빈 문자열을 먼저 안전하게 거른 뒤, True/False 부울 값과 문자열을 모두 올바르게 처리할 수 있도록 개선해 주세요.
| def _yn_to_label(value: object) -> str | None: | |
| text = str(value or "").strip().upper() | |
| if not text: | |
| return None | |
| if text in {"Y", "YES", "1", "가능", "O"}: | |
| return "가능" | |
| if text in {"N", "NO", "0", "불가", "X"}: | |
| return "불가" | |
| if len(text) <= 20: | |
| return text | |
| return None | |
| def _yn_to_label(value: object) -> str | None: | |
| if value is None or value == "": | |
| return None | |
| text = str(value).strip().upper() | |
| if text in {"Y", "YES", "1", "가능", "O", "TRUE"}: | |
| return "가능" | |
| if text in {"N", "NO", "0", "불가", "X", "FALSE"}: | |
| return "불가" | |
| if len(text) <= 20: | |
| return text | |
| return None |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive pipeline for fetching, transforming, and rendering policy news and festival data into card news format, including new API clients, database handoff services, and Pillow-based template rendering modules. The review feedback highlights several key improvement opportunities: first, calendar date validation should be strictly enforced using datetime.strptime in date utility functions to prevent potential internal server errors; second, sequential image downloads should be parallelized using asyncio.gather to optimize network performance; and third, a broad substring check for URLs in the policy transformation service should be removed to prevent false-positive deduplication of different URLs.
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.
| def validate_yyyymmdd(value: str, *, label: str) -> str: | ||
| text = (value or "").strip() | ||
| if len(text) != 8 or not text.isdigit(): | ||
| raise ValueError(f"{label}는 YYYYMMDD 8자리여야 합니다 (받음: {value!r})") | ||
| return text |
There was a problem hiding this comment.
validate_yyyymmdd 함수가 실제 유효한 날짜인지 검증하지 않아, 잘못된 날짜 형식 입력 시 내부 로직에서 예외가 발생하고 500 에러로 이어질 수 있습니다. datetime.strptime을 활용해 실제 유효한 날짜인지 검증하는 로직을 추가하는 것을 권장합니다.
| def validate_yyyymmdd(value: str, *, label: str) -> str: | |
| text = (value or "").strip() | |
| if len(text) != 8 or not text.isdigit(): | |
| raise ValueError(f"{label}는 YYYYMMDD 8자리여야 합니다 (받음: {value!r})") | |
| return text | |
| from datetime import datetime | |
| def validate_yyyymmdd(value: str, *, label: str) -> str: | |
| text = (value or "").strip() | |
| if len(text) != 8 or not text.isdigit(): | |
| raise ValueError(f"{label}는 YYYYMMDD 8자리여야 합니다 (받음: {value!r})") | |
| try: | |
| datetime.strptime(text, "%Y%m%d") | |
| except ValueError: | |
| raise ValueError(f"{label}는 유효한 날짜여야 합니다 (받음: {value!r})") | |
| return text |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive system for generating policy card news slides, including OpenAPI integration via PolicyNewsClient, layout templates, Pillow-based rendering, and Gemini-powered content transformation. The review feedback highlights several critical improvements: removing duplicate configuration blocks for visitkorea and festival in app/core/config.py, utilizing the configured policy_cardnews_font_dir in _load_font, cleaning up duplicate imports in app/core/deps.py and app/main.py, and resolving a potential text overflow issue in wrap_text when individual words exceed the maximum width.
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.
정책뉴스 수집→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 절대 경로 처리를 보완한다.
/policy-admin 접근 제어와 정책 파이프라인 캡슐화를 보강하고, 카드뉴스 정리 및 nested transaction 실패 처리의 데이터 유실 위험을 줄인다. Co-authored-by: Cursor <cursoragent@cursor.com>
/policy-admin 날짜 검증 예외가 400으로 처리되도록 보완하고, Community.pin_id nullable 변경으로 기존 데이터 마이그레이션 리스크를 줄인다.
handoff 적재와 sync_pipeline의 중복 DB 조회를 줄이고, 카드뉴스 파일 읽기·파이프라인 정리 작업을 스레드 풀에서 실행해 이벤트 루프 블로킹을 방지한다.
sync가 remaining_pending 기준으로 배치를 끝까지 돌고, import 후 캐시 정리로 handoff가 비어도 DB 적재 완료로 처리하도록 status·hint를 보강한다.
feat: 정책 핀 sync·배치 가공·DB 적재 파이프라인 및 관리자 API 추가
selnem
left a comment
There was a problem hiding this comment.
LGTM! 기능 머지 완료하였습니다 확인도 다하였고 머지하시면 될것 같습니다.
🔗 Related Issue
✨ 작업 개요
1. 정책 카드뉴스 고정 템플릿 구조 추가
template_covertemplate_numberedtemplate_three_coltemplate_gridtemplate_cta정책 카드뉴스 슬라이드 유형을 고정 템플릿 기반으로 분리했습니다.
2. 템플릿 팔레트 관리 로직 구현
royal_blue,coral_sunset,mint_pop등 템플릿 전용 팔레트 정의3. 슬라이드 레이아웃 자동 결정 로직 구현
4. 마스코트 영역 계산 및 배치 로직 개선
5. 템플릿 렌더링 Context 정리
TemplateContext를 통해 슬라이드, 마스코트, 팔레트, 원문 링크, 대표 이미지 정보를 한 번에 전달체크리스트
📷 이미지 첨부 (선택)
🧐 집중 리뷰 요청