Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2699982
refactor: ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€
qkwltkwkd1 Jun 2, 2026
eda612f
refactor: ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐ
qkwltkwkd1 Jun 2, 2026
bc9c55f
refactor: ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
qkwltkwkd1 Jun 2, 2026
7fbb8e7
refactor: AsyncClient ์ˆ˜์ •
qkwltkwkd1 Jun 2, 2026
6f7d7ec
refactor: ์œ ํšจํ•œ ๋‚ ์งœ์ธ์ง€ ๊ฒ€์ฆ
qkwltkwkd1 Jun 2, 2026
077d082
refactor: ์ฒ˜๋ฆฌ ์†๋„ ํ–ฅ์ƒ
qkwltkwkd1 Jun 2, 2026
4dc0f06
refactor: ์ค‘๋ณต ์ œ๊ฑฐ
qkwltkwkd1 Jun 2, 2026
2a226e7
fix: ์ถฉ๋Œ ํ•ด๊ฒฐ
qkwltkwkd1 Jun 2, 2026
5032b91
fix: ci ํ•ด๊ฒฐ
qkwltkwkd1 Jun 2, 2026
73d99ed
fix: ์ถฉ๋Œ ํ•ด๊ฒฐ
qkwltkwkd1 Jun 4, 2026
1a82afe
refactor: ์ค‘๋ณต ๋ธ”๋ก ์ œ๊ฑฐ
qkwltkwkd1 Jun 9, 2026
3aed1aa
refactor: ํฐํŠธ ํ•˜๋“œ์ฝ”๋”ฉ ์ˆ˜์ •
qkwltkwkd1 Jun 9, 2026
b4d0e68
refactor: ์ž„ํฌํŠธ ์ •๋ฆฌ
qkwltkwkd1 Jun 9, 2026
345b892
refactor: ์ž„ํฌํŠธ ์ •๋ฆฌ
qkwltkwkd1 Jun 9, 2026
daab579
refactor: fallback wrapping ๋˜๋„๋ก ๋ณด์™„
qkwltkwkd1 Jun 9, 2026
52142b7
feat: ์ •์ฑ… ํ•€ syncยท๋ฐฐ์น˜ ๊ฐ€๊ณตยทDB ์ ์žฌ ํŒŒ์ดํ”„๋ผ์ธ ๋ฐ ๊ด€๋ฆฌ์ž API ์ถ”๊ฐ€
selnem Jun 12, 2026
1a29a06
fix: ์ •์ฑ… ํ•€ ํŒŒ์ดํ”„๋ผ์ธ ์ฝ”๋“œ๋ฆฌ๋ทฐ ์ง€์  4๊ฑด ๋ฐ˜์˜
selnem Jun 12, 2026
6eec2df
fix: import_handoff_batch ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์˜คํƒ€ ์ˆ˜์ • ๋ฐ ์ •์ฑ… ํŒŒ์ดํ”„๋ผ์ธ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
selnem Jun 12, 2026
dc70187
fix: ์ •์ฑ… ํ•€ sync ๋‚ ์งœ ๊ฒ€์ฆยทhandoff ์ ์žฌ drainยท๋ชจ๋ธ unique ๋™๊ธฐํ™”
selnem Jun 12, 2026
89e4707
fix: ์ •์ฑ… ๊ด€๋ฆฌ์ž API ๋ฆฌ๋ทฐ ์ง€์  ๋ณด์™„
selnem Jun 12, 2026
1b97e63
fix: ์ •์ฑ… ๊ด€๋ฆฌ์ž ๋ฆฌ๋ทฐ ํ›„์† ์ง€์  ๋ฐ˜์˜
selnem Jun 12, 2026
f1fc0a3
fix: ์ •์ฑ… ํŒŒ์ดํ”„๋ผ์ธ N+1 ์ฟผ๋ฆฌยท๋™๊ธฐ I/O ๋ฆฌ๋ทฐ ๋ฐ˜์˜
selnem Jun 12, 2026
5f4bade
fix: ์ •์ฑ… sync ๋ฃจํ”„ยทhandoff ๋น„์–ด ์žˆ์„ ๋•Œ DB ์™„๋ฃŒ ์ƒํƒœ ์ธ์‹
selnem Jun 12, 2026
bdf3ab6
Merge pull request #118 from IssueIssyu/feat/policy-api-db-insert
selnem Jun 12, 2026
386f58a
Merge pull request #86 from IssueIssyu/feat/72-policy-api
selnem Jun 13, 2026
3904d81
Merge branch 'develop' into feat/104-contest-crawler
selnem Jun 13, 2026
06e0af1
refactor: configยทdeps ์ค‘๋ณต ์ •๋ฆฌ ๋ฐ Gemini/๋ฏผ์› wiring ๊ณตํ†ตํ™”
selnem Jun 13, 2026
57ff93e
feat: ๊ณต๋ชจ์ „ ํ•€ ํฌ๋กคยท์นด๋“œ๋‰ด์ŠคยทDB ์ ์žฌ ํŒŒ์ดํ”„๋ผ์ธ ์ถ”๊ฐ€
selnem Jun 13, 2026
855bf91
fix: ComplaintPetition ๋ฏธ์‚ฌ์šฉ ORM ์ปฌ๋Ÿผ ๋งคํ•‘ ์ œ๊ฑฐ
selnem Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions app/contest_cardnews/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,10 @@
wrap_text,
)

_REPO_ROOT = Path(__file__).resolve().parents[3]
from app.policy_cardnews.paths import contest_cardnews_font_dir


def _get_font_dir() -> Path:
from app.core.config import settings

if settings.policy_cardnews_font_dir:
return Path(settings.policy_cardnews_font_dir)
return _REPO_ROOT / "app" / "assets" / "fonts"


_FONT_DIR = _get_font_dir()
_FONT_DIR = contest_cardnews_font_dir()
_CONTEST_FONT_REGULAR = "Hakgyoansim Dunggeunmiso TTF R.ttf"
_CONTEST_FONT_BOLD = "Hakgyoansim Dunggeunmiso TTF B.ttf"

Expand Down
215 changes: 156 additions & 59 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,23 @@ class Settings(BaseSettings):
alias="GEMINI_PIN_TEXT_MODEL",
)
rag_enable_rerank: bool = Field(
default=True,
default=False,
alias="RAG_ENABLE_RERANK",
)
rag_vector_query_mode: str = Field(
default="hybrid",
alias="RAG_VECTOR_QUERY_MODE",
)
rag_retrieve_top_k: int = Field(
default=8,
default=10,
ge=1,
le=50,
le=100,
alias="RAG_RETRIEVE_TOP_K",
)
rag_rerank_top_k: int = Field(
default=5,
ge=1,
le=20,
le=100,
alias="RAG_RERANK_TOP_K",
)
gemini_pin_text_fallback_models: str = Field(
Expand Down Expand Up @@ -159,6 +159,116 @@ class Settings(BaseSettings):
alias="VECTOR_TEXT_SEARCH_CONFIG",
)

# ๋ฌธํ™”์ฒด์œก๊ด€๊ด‘๋ถ€ ์ •์ฑ…๋ธŒ๋ฆฌํ•‘ ์ •์ฑ…๋‰ด์Šค OpenAPI (๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ)
policy_news_service_key: SecretStr | None = Field(
default=None,
alias="POLICY_NEWS_SERVICE_KEY",
description="์ •์ฑ…๋‰ด์Šค ์ „์šฉ API ํ‚ค (์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ํ‚ค, ํ•„์ˆ˜)",
)
policy_news_api_base_url: str = Field(
default="http://apis.data.go.kr/1371000/policyNewsService",
alias="POLICY_NEWS_API_BASE_URL",
)
policy_news_request_timeout_seconds: float = Field(
default=30.0,
gt=0,
alias="POLICY_NEWS_REQUEST_TIMEOUT_SECONDS",
)
policy_news_request_interval_seconds: float = Field(
default=0.15,
ge=0,
alias="POLICY_NEWS_REQUEST_INTERVAL_SECONDS",
)
policy_sync_lookback_days: int = Field(
default=3,
ge=1,
le=30,
alias="POLICY_SYNC_LOOKBACK_DAYS",
description="๋ฐฐ์น˜ ์ˆ˜์ง‘ ์‹œ ์˜ค๋Š˜ ํฌํ•จ N์ผ (API 3์ผ ์ œํ•œ ๊ณ ๋ ค)",
)
policy_sync_interval_days: int = Field(
default=3,
ge=1,
le=30,
alias="POLICY_SYNC_INTERVAL_DAYS",
description="์ž๋™ sync ์ตœ์†Œ ๊ฐ„๊ฒฉ(์ผ)",
)
policy_sync_schedule_hour_kst: int = Field(
default=1,
ge=0,
le=23,
alias="POLICY_SYNC_SCHEDULE_HOUR_KST",
description="์ •์ฑ… ํ•€ sync ์Šค์ผ€์ค„ ํ™•์ธ ์‹œ๊ฐ (KST)",
)
policy_admin_user_name: str = Field(
default="admin",
validation_alias=AliasChoices("POLICY_ADMIN_USER_NAME", "POLICY_ADMIN_NICKNAME"),
description="์ •์ฑ… ํ•€ ๋“ฑ๋ก์— ์‚ฌ์šฉํ•  user.user_name",
)
policy_sync_batch_size: int = Field(
default=5,
ge=1,
le=25,
alias="POLICY_SYNC_BATCH_SIZE",
description="sync/transform/import 1ํšŒ ๋ฐฐ์น˜ ๊ฑด์ˆ˜",
)
policy_transform_concurrency: int = Field(
default=3,
ge=1,
le=10,
alias="POLICY_TRANSFORM_CONCURRENCY",
description="์ •์ฑ… pin_contentยท์นด๋“œ๋‰ด์Šค Gemini ๊ฐ€๊ณต ๋™์‹œ ํ˜ธ์ถœ ์ˆ˜",
)
policy_prune_pipeline_after_import: bool = Field(
default=True,
alias="POLICY_PRUNE_PIPELINE_AFTER_IMPORT",
description="DB INSERT ์„ฑ๊ณต ํ›„ JSONLยท๋กœ์ปฌ ์นด๋“œ๋‰ด์Šค ์บ์‹œ ์ œ๊ฑฐ",
)
policy_cardnews_keep_local_files: bool = Field(
default=False,
alias="POLICY_CARDNEWS_KEEP_LOCAL_FILES",
description="True๋ฉด S3 ์—…๋กœ๋“œ ํ›„์—๋„ rag/output/policy_cardnews ์œ ์ง€",
)
policy_sync_merge_documents: bool = Field(
default=False,
alias="POLICY_SYNC_MERGE_DOCUMENTS",
description="False๋ฉด sync ์ˆ˜์ง‘ ์‹œ policy_documents.jsonl์„ ์ด๋ฒˆ ๊ตฌ๊ฐ„์œผ๋กœ ๋ฎ์–ด์”€",
)
policy_cardnews_s3_prefix: str = Field(
default="policy-cardnews",
alias="POLICY_CARDNEWS_S3_PREFIX",
description="์ •์ฑ… ์นด๋“œ๋‰ด์Šค S3 object key prefix",
)
gemini_cardnews_image_model: str = Field(
default="gemini-2.5-flash-image",
alias="GEMINI_CARDNEWS_IMAGE_MODEL",
description="์ •์ฑ… ์นด๋“œ๋‰ด์Šค ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ชจ๋ธ",
)
gemini_cardnews_image_fallback_models: str = Field(
default="gemini-3-pro-image-preview,imagen-3.0-generate-002",
alias="GEMINI_CARDNEWS_IMAGE_FALLBACK_MODELS",
)
policy_cardnews_pillow_fallback: bool = Field(
default=True,
alias="POLICY_CARDNEWS_PILLOW_FALLBACK",
description="์ด๋ฏธ์ง€ ๋ชจ๋ธ ์‹คํŒจ ์‹œ Pillow ํ…œํ”Œ๋ฆฟ ํ•ฉ์„ฑ์œผ๋กœ ํด๋ฐฑ",
)
policy_cardnews_use_template: bool = Field(
default=True,
alias="POLICY_CARDNEWS_USE_TEMPLATE",
description="๊ณ ์ • ์นด๋“œ๋‰ด์Šค ํ…œํ”Œ๋ฆฟ(๋ ˆํผ๋Ÿฐ์Šค ํ˜•์‹) ์‚ฌ์šฉ",
)
policy_cardnews_use_image_model: bool = Field(
default=False,
alias="POLICY_CARDNEWS_USE_IMAGE_MODEL",
description="True๋ฉด Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ, False๋ฉด Pillow SNS ํ…œํ”Œ๋ฆฟ",
)
policy_cardnews_font_dir: str = Field(
default="../assets/fonts",
alias="POLICY_CARDNEWS_FONT_DIR",
description="Pretendard ๋“ฑ ํฐํŠธ ํด๋” (app/policy_cardnews ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ)",
)

# ํ•œ๊ตญ๊ด€๊ด‘๊ณต์‚ฌ TourAPI (๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ ํ™œ์šฉ์‹ ์ฒญ ํ‚ค)
visitkorea_service_key: SecretStr | None = Field(
default=None,
Expand Down Expand Up @@ -213,85 +323,72 @@ class Settings(BaseSettings):
alias="FESTIVAL_BATCH_SIZE",
description="admin fetch/transform/import ๊ธฐ๋ณธ ๋ฐฐ์น˜ ํฌ๊ธฐ",
)

# ๋ฌธํ™”์ฒด์œก๊ด€๊ด‘๋ถ€ ์ •์ฑ…๋ธŒ๋ฆฌํ•‘ ์ •์ฑ…๋‰ด์Šค OpenAPI (๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ)
policy_news_service_key: SecretStr | None = Field(
default=None,
alias="POLICY_NEWS_SERVICE_KEY",
description="์ •์ฑ…๋‰ด์Šค ์ „์šฉ API ํ‚ค (์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ํ‚ค, ํ•„์ˆ˜)",
)
policy_news_api_base_url: str = Field(
default="http://apis.data.go.kr/1371000/policyNewsService",
alias="POLICY_NEWS_API_BASE_URL",
)
policy_news_request_timeout_seconds: float = Field(
default=30.0,
gt=0,
alias="POLICY_NEWS_REQUEST_TIMEOUT_SECONDS",
policy_cardnews_mascot_dir: str | None = Field(
default="../assets/mascots",
alias="POLICY_CARDNEWS_MASCOT_DIR",
description="ํ•€ ์บ๋ฆญํ„ฐ PNG ํด๋” (app/policy_cardnews ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ). mascots.json files ๋ชฉ๋ก์— ์žˆ๋Š” PNG๋งŒ ์‚ฌ์šฉ",
)
policy_news_request_interval_seconds: float = Field(
default=0.15,
contest_sync_schedule_hour_kst: int = Field(
default=12,
ge=0,
alias="POLICY_NEWS_REQUEST_INTERVAL_SECONDS",
le=23,
alias="CONTEST_SYNC_SCHEDULE_HOUR_KST",
description="๊ณต๋ชจ์ „ ํ•€ sync ์Šค์ผ€์ค„ ์‹œ๊ฐ (KST)",
)
policy_sync_lookback_days: int = Field(
default=3,
contest_crawl_max_pages: int = Field(
default=1,
ge=1,
le=30,
alias="POLICY_SYNC_LOOKBACK_DAYS",
description="๋ฐฐ์น˜ ์ˆ˜์ง‘ ์‹œ ์˜ค๋Š˜ ํฌํ•จ N์ผ (API 3์ผ ์ œํ•œ ๊ณ ๋ ค)",
le=50,
alias="CONTEST_CRAWL_MAX_PAGES",
description="sync/crawl ๊ธฐ๋ณธ ๋ชฉ๋ก ํŽ˜์ด์ง€ ์ˆ˜",
)
gemini_cardnews_image_model: str = Field(
default="gemini-2.5-flash-image",
alias="GEMINI_CARDNEWS_IMAGE_MODEL",
description="์ •์ฑ… ์นด๋“œ๋‰ด์Šค ์Šฌ๋ผ์ด๋“œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ชจ๋ธ",
contest_sync_batch_size: int = Field(
default=5,
ge=1,
le=25,
alias="CONTEST_SYNC_BATCH_SIZE",
description="sync/transform/import 1ํšŒ ๋ฐฐ์น˜ ๊ฑด์ˆ˜",
)
gemini_cardnews_image_fallback_models: str = Field(
default="gemini-3-pro-image-preview,imagen-3.0-generate-002",
alias="GEMINI_CARDNEWS_IMAGE_FALLBACK_MODELS",
contest_transform_concurrency: int = Field(
default=3,
ge=1,
le=10,
alias="CONTEST_TRANSFORM_CONCURRENCY",
description="๊ณต๋ชจ์ „ pin_contentยท์นด๋“œ๋‰ด์Šค Gemini ๊ฐ€๊ณต ๋™์‹œ ํ˜ธ์ถœ ์ˆ˜",
)
policy_cardnews_pillow_fallback: bool = Field(
default=True,
alias="POLICY_CARDNEWS_PILLOW_FALLBACK",
description="์ด๋ฏธ์ง€ ๋ชจ๋ธ ์‹คํŒจ ์‹œ Pillow ํ…œํ”Œ๋ฆฟ ํ•ฉ์„ฑ์œผ๋กœ ํด๋ฐฑ",
contest_admin_user_name: str = Field(
default="admin",
validation_alias=AliasChoices("CONTEST_ADMIN_USER_NAME", "CONTEST_ADMIN_NICKNAME"),
description="๊ณต๋ชจ์ „ ํ•€ ๋“ฑ๋ก์— ์‚ฌ์šฉํ•  user.user_name",
)
policy_cardnews_use_template: bool = Field(
contest_prune_pipeline_after_import: bool = Field(
default=True,
alias="POLICY_CARDNEWS_USE_TEMPLATE",
description="๊ณ ์ • ์นด๋“œ๋‰ด์Šค ํ…œํ”Œ๋ฆฟ(๋ ˆํผ๋Ÿฐ์Šค ํ˜•์‹) ์‚ฌ์šฉ",
alias="CONTEST_PRUNE_PIPELINE_AFTER_IMPORT",
description="DB INSERT ์„ฑ๊ณต ํ›„ JSONLยท๋กœ์ปฌ ์นด๋“œ๋‰ด์Šค ์บ์‹œ ์ œ๊ฑฐ",
)
policy_cardnews_use_image_model: bool = Field(
contest_cardnews_keep_local_files: bool = Field(
default=False,
alias="POLICY_CARDNEWS_USE_IMAGE_MODEL",
description="True๋ฉด Gemini ์ด๋ฏธ์ง€ ๋ชจ๋ธ, False๋ฉด Pillow SNS ํ…œํ”Œ๋ฆฟ",
)
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")
policy_cardnews_font_dir: str | None = Field(
default=None,
alias="POLICY_CARDNEWS_FONT_DIR",
description="Pretendard ๋“ฑ TTF ํด๋” (๊ธฐ๋ณธ app/assets/fonts)",
alias="CONTEST_CARDNEWS_KEEP_LOCAL_FILES",
description="True๋ฉด S3 ์—…๋กœ๋“œ ํ›„์—๋„ rag/output/contest_cardnews ์œ ์ง€",
)
policy_cardnews_mascot_dir: str | None = Field(
default="app/assets/mascots",
alias="POLICY_CARDNEWS_MASCOT_DIR",
description="ํ•€ ์บ๋ฆญํ„ฐ PNG ํด๋”. mascots.json files ๋ชฉ๋ก์— ์žˆ๋Š” PNG๋งŒ ์‚ฌ์šฉ",
contest_cardnews_s3_prefix: str = Field(
default="contest-cardnews",
alias="CONTEST_CARDNEWS_S3_PREFIX",
description="๊ณต๋ชจ์ „ ์นด๋“œ๋‰ด์Šค S3 object key prefix",
)

@field_validator("policy_cardnews_font_dir", mode="before")
@classmethod
def _empty_string_policy_cardnews_font_dir(cls, value: object) -> object:
if value == "":
return None
return "../assets/fonts"
return value

@field_validator("policy_cardnews_mascot_dir", mode="before")
@classmethod
def _empty_string_policy_cardnews_mascot_dir(cls, value: object) -> object:
if value == "":
return None
return "../assets/mascots"
return value

@field_validator("gemini_embedding_batch_size", mode="before")
Expand Down
Loading
Loading