From 519ed4a79b5e2f6c61912f521bc64ce86cc52c5d Mon Sep 17 00:00:00 2001 From: hbyang Date: Thu, 1 Oct 2026 10:52:31 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/backend,deploy:=20=EB=AF=B8?= =?UTF-8?q?=EB=8B=88=EB=B8=94=EB=A1=9C=EA=B7=B8=20=EC=9E=AC=EA=B3=A0=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B1=84=EC=9A=B0=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=8A=A4=EC=9C=84=EC=B9=98=EB=A1=9C=20=EA=B2=A9=EB=A6=AC=20?= =?UTF-8?q?=E2=80=94=20=EA=B8=B0=EB=B3=B8=20=EA=BA=BC=EC=A7=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-10-01 대표 지시("이거는 안보내도 될것같아"). 켜면 발행된 사이트 **전체**가 대상이고 업장당 최대 30건이라 첫 회차에 AI 호출이 한꺼번에 일어난다 — 그 비용은 사람이 정할 일이지 기본값이 정할 일이 아니다. 글은 빌더의 [지금 생성하기] 로 사장님이 직접 만든다. ★ add_job 을 다시 지우지 않고 스위치로 뺀 이유: 바로 앞 커밋(4650125)이 고친 게 "등록 자체가 없는데 주석만 새벽에 채운다고 말하는" 상태였다. 지우면 그 상태로 돌아가 **끈 것과 빠뜨린 것이 구별되지 않는다** — 증상("메일이 안 온다")을 보고 발송 쪽을 뒤지게 된다. 지금은 꺼져 있다는 사실이 코드에 남는다. ★ 발송(09:00)은 이 스위치와 무관하게 항상 돈다 — 손으로 만든 글도 나가야 한다. - scheduler/__init__: BLOG_AUTO_DRAFTS_ENABLED=1 일 때만 blog-drafts 등록 - docker-compose · .env.example: 스위치 전달, 기본 0 - docs/MINI_BLOG 2절: 기본 꺼짐과 그 이유, 예전에 등록이 없었다는 실측 테스트 5건(2건 신설 — 기본 꺼짐 · 켜면 등록됨), 전부 통과 Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 7 ++++ docker-compose.yml | 3 ++ docs/MINI_BLOG.md | 8 ++++ solution/backend/scheduler/__init__.py | 23 ++++++---- .../tests/test_scheduler_registration.py | 42 ++++++++++++------- 5 files changed, 61 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index b6006c3..b9ca461 100644 --- a/.env.example +++ b/.env.example @@ -104,6 +104,13 @@ KAKAO_EVENT_DEV=0 # ★ 오픈빌더에 이벤트 블록(스킬 연결)을 만들고 배포하기 전에는 켜지 않는다 — 켜면 # 사장님 카톡에 빈 말풍선이 간다. 기본 꺼짐. KAKAO_APPROVAL_PUSH_ENABLED=0 + +# 미니 블로그 재고 **자동** 채우기(매일 04:10 KST). 기본은 꺼짐 — 글은 빌더의 +# [지금 생성하기] 로 사장님이 직접 만든다(2026-10-01 결정, docs/MINI_BLOG.md). +# ★ 켜면 발행된 사이트 **전체**가 대상이고 업장당 최대 30건이라, 첫 회차에 AI 호출이 +# 한꺼번에 일어난다. 비용을 확인하고 켠다. +# ★ 발송(09:00)은 이 스위치와 무관하게 항상 돈다 — 손으로 만든 글도 나가야 한다. +BLOG_AUTO_DRAFTS_ENABLED=0 KAKAO_APPROVAL_EVENT_NAME=post_approval # [승인] 버튼이 부르는 블록 ID(오픈빌더 블록 주소의 /intent/). 그 블록에 우리 스킬이 # 스킬데이터로 연결돼 있어야 한다. ★ 비우면 [승인] 버튼을 그리지 않는다(수정하기만 나간다). diff --git a/docker-compose.yml b/docker-compose.yml index 514e6bf..705b104 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,6 +57,9 @@ services: <<: *common-env # ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다. SCHEDULER_ENABLED: "1" + # 미니 블로그 재고 자동 채우기(04:10 KST). 기본 꺼짐 — 켜면 발행 사이트 전체에 + # 업장당 30건씩 AI 호출이 일어난다(docs/MINI_BLOG.md 2절). 발송(09:00)은 무관하게 돈다. + BLOG_AUTO_DRAFTS_ENABLED: ${BLOG_AUTO_DRAFTS_ENABLED:-0} # ★ GSC_CREDENTIALS_HOST_FILE(호스트 경로, .env)을 아래 볼륨으로 이 컨테이너 안에 마운트한 # 고정 자리다. search_console_settings.load_settings() 가 실제로 읽는 건 이 값이다 — # 호스트 경로를 코드에 그대로 넘기면 컨테이너 안에서 그 경로가 없어 실패한다. diff --git a/docs/MINI_BLOG.md b/docs/MINI_BLOG.md index 740898e..ed70f90 100644 --- a/docs/MINI_BLOG.md +++ b/docs/MINI_BLOG.md @@ -61,6 +61,14 @@ `scheduler/__init__.py` 에 `add_job` 한 줄, 로직은 `scheduler/jobs.py` → `services/blog_service.py`. - **주기**: 하루 1회(KST 새벽 04:10). `SCHEDULER_ENABLED=1` 인 프로세스에서만 돈다(이미 그 규약이다) +- ★★ **기본으로 돌지 않는다** — `BLOG_AUTO_DRAFTS_ENABLED=1` 이어야 등록된다 + (2026-10-01 대표 지시: "이거는 안보내도 될것같아"). 글은 빌더의 [지금 생성하기] 로 + 사장님이 직접 만든다. 켜면 발행된 사이트 **전체**가 대상이라 첫 회차에 AI 호출이 + 한꺼번에 일어난다 — 그 비용은 사람이 정할 일이다. + ★ **발송(09:00)은 이 스위치와 무관하게 항상 돈다.** 손으로 만든 글도 나가야 한다. + ★ 2026-09-30 까지는 이 잡이 **등록 자체가 없었다** — 함수와 주석("새벽에 채운다")만 있고 + `add_job` 이 없어 한 번도 돈 적이 없다. 증상은 "메일이 안 온다" 로만 보이고 실제로는 + 보낼 글이 0건이었다. 지금은 꺼져 있다는 사실이 코드에 남는다(`scheduler/__init__.py`) - **대상**: 발행된 사이트 중 재고(DRAFT+REVIEWED)가 `REFILL_BELOW`(30) 미만인 업장 — 하루 한 통씩 나간다고 보면 한 달치를 채우는 셈이다 - **한 번에 `BATCH_SIZE`(30)건**씩. 앞 회차의 `topic_key` 목록을 프롬프트에 넣어 중복을 막는다 diff --git a/solution/backend/scheduler/__init__.py b/solution/backend/scheduler/__init__.py index 2a18df4..a82afeb 100644 --- a/solution/backend/scheduler/__init__.py +++ b/solution/backend/scheduler/__init__.py @@ -40,14 +40,21 @@ def start_scheduler(): id="alert-outbox", max_instances=1, coalesce=True) _scheduler.add_job(sweep_queue_health, "interval", minutes=5, id="queue-health", max_instances=1, coalesce=True) - # 미니 블로그 — 새벽에 재고를 채우고, 아침에 검수 통과분을 보낸다(docs/MINI_BLOG.md). - # ★★ 재고 채우기가 **등록돼 있지 않았다**(실측 2026-09-30). 주석만 "새벽에 채운다" 라고 - # 말하고 `sweep_blog_drafts` 는 어디에서도 불리지 않아, 자동 생성이 한 번도 돈 적이 없다. - # 증상이 "메일이 안 온다" 로만 보여서 발송 쪽을 보게 된다 — 실제로는 **보낼 글이 - # 0건**이었고, 지금 쌓여 있는 글은 전부 화면의 [지금 생성하기] 로 손으로 만든 것이다. - # 발송(09:00)보다 앞서야 그날 아침에 나갈 재고가 있다. - _scheduler.add_job(sweep_blog_drafts, "cron", hour=4, minute=10, - id="blog-drafts", max_instances=1, coalesce=True) + # 미니 블로그 — 아침에 검수 통과분을 보낸다(docs/MINI_BLOG.md). + # + # ★★ 재고 **자동** 채우기는 기본으로 돌지 않는다(2026-10-01 대표 지시: "이거는 안보내도 + # 될것같아"). 글은 빌더 화면의 [지금 생성하기] 로 사장님이 직접 만든다. + # 켜면 발행된 사이트 **전체**가 대상이고 업장당 최대 30건이라, 첫 회차에 AI 호출이 + # 한꺼번에 일어난다 — 그 비용을 감수할지는 사람이 정할 일이지 기본값이 정할 일이 아니다. + # + # ★ 그래서 `add_job` 을 지우지 않고 스위치로 격리했다. 예전에는 **등록 자체가 없었고** + # 주석만 "새벽에 채운다" 라고 말했다(실측 2026-09-30) — 그 상태는 "끈 것" 과 "빠뜨린 것" + # 이 구별되지 않아, 증상("메일이 안 온다")을 보고 발송 쪽을 뒤지게 된다. + # 실제로는 보낼 글이 0건이었다. 지금은 꺼져 있다는 사실이 코드에 남는다. + if os.environ.get("BLOG_AUTO_DRAFTS_ENABLED") == "1": + # 발송(09:00)보다 앞서야 그날 아침에 나갈 재고가 있다. + _scheduler.add_job(sweep_blog_drafts, "cron", hour=4, minute=10, + id="blog-drafts", max_instances=1, coalesce=True) _scheduler.add_job(sweep_blog_mail, "cron", hour=9, minute=0, id="blog-mail", max_instances=1, coalesce=True) _scheduler.start() diff --git a/solution/backend/tests/test_scheduler_registration.py b/solution/backend/tests/test_scheduler_registration.py index 2680273..3495675 100644 --- a/solution/backend/tests/test_scheduler_registration.py +++ b/solution/backend/tests/test_scheduler_registration.py @@ -12,26 +12,40 @@ import scheduler @pytest.fixture -def started(monkeypatch): - monkeypatch.setenv("SCHEDULER_ENABLED", "1") - # 실제로 돌리지 않는다 — 등록만 본다. - monkeypatch.setattr("apscheduler.schedulers.asyncio.AsyncIOScheduler.start", lambda self: None) - scheduler._scheduler = None - scheduler.start_scheduler() - yield scheduler._scheduler +def start(monkeypatch): + """실제로 돌리지 않는다 — 등록만 본다.""" + def _start(**env): + monkeypatch.setenv("SCHEDULER_ENABLED", "1") + for k, v in env.items(): + monkeypatch.setenv(k, v) + monkeypatch.setattr("apscheduler.schedulers.asyncio.AsyncIOScheduler.start", lambda self: None) + scheduler._scheduler = None + scheduler.start_scheduler() + return scheduler._scheduler + + yield _start scheduler._scheduler = None -def test_미니블로그는_생성과_발송이_둘_다_등록된다(started): - """★ 발송만 있으면 보낼 글이 영원히 0건이다.""" - ids = {j.id for j in started.get_jobs()} - assert "blog-drafts" in ids, "재고 채우기가 등록되지 않았다 — 자동 생성이 안 돈다" - assert "blog-mail" in ids +def test_발송은_스위치_없이도_등록된다(start): + """메일·카톡 발송은 항상 돈다 — 사장님이 직접 만든 글도 나가야 한다.""" + assert "blog-mail" in {j.id for j in start().get_jobs()} -def test_재고_채우기가_발송보다_앞선다(started): +def test_자동_재고채우기는_기본으로_꺼져_있다(start): + """★ 2026-10-01 대표 지시. 켜면 발행 사이트 전체에 업장당 30건씩 AI 호출이 일어난다 — + 기본값이 정할 일이 아니다.""" + assert "blog-drafts" not in {j.id for j in start().get_jobs()} + + +def test_스위치를_켜면_등록된다(start): + """★ 빠뜨린 것과 끈 것은 다르다 — 켜지는지까지 봐야 '격리했다' 가 성립한다.""" + assert "blog-drafts" in {j.id for j in start(BLOG_AUTO_DRAFTS_ENABLED="1").get_jobs()} + + +def test_재고_채우기가_발송보다_앞선다(start): """그날 아침에 나갈 글이 있으려면 생성이 먼저여야 한다.""" - by_id = {j.id: j for j in started.get_jobs()} + by_id = {j.id: j for j in start(BLOG_AUTO_DRAFTS_ENABLED="1").get_jobs()} hour = lambda j: int(str(by_id[j].trigger.fields[by_id[j].trigger.FIELD_NAMES.index("hour")])) assert hour("blog-drafts") < hour("blog-mail")