[fix] solution/backend,deploy: 미니블로그 재고 자동 채우기를 스위치로 격리 — 기본 꺼짐
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) <noreply@anthropic.com>
This commit is contained in:
parent
4650125c6c
commit
519ed4a79b
@ -104,6 +104,13 @@ KAKAO_EVENT_DEV=0
|
|||||||
# ★ 오픈빌더에 이벤트 블록(스킬 연결)을 만들고 배포하기 전에는 켜지 않는다 — 켜면
|
# ★ 오픈빌더에 이벤트 블록(스킬 연결)을 만들고 배포하기 전에는 켜지 않는다 — 켜면
|
||||||
# 사장님 카톡에 빈 말풍선이 간다. 기본 꺼짐.
|
# 사장님 카톡에 빈 말풍선이 간다. 기본 꺼짐.
|
||||||
KAKAO_APPROVAL_PUSH_ENABLED=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
|
KAKAO_APPROVAL_EVENT_NAME=post_approval
|
||||||
# [승인] 버튼이 부르는 블록 ID(오픈빌더 블록 주소의 /intent/<ID>). 그 블록에 우리 스킬이
|
# [승인] 버튼이 부르는 블록 ID(오픈빌더 블록 주소의 /intent/<ID>). 그 블록에 우리 스킬이
|
||||||
# 스킬데이터로 연결돼 있어야 한다. ★ 비우면 [승인] 버튼을 그리지 않는다(수정하기만 나간다).
|
# 스킬데이터로 연결돼 있어야 한다. ★ 비우면 [승인] 버튼을 그리지 않는다(수정하기만 나간다).
|
||||||
|
|||||||
@ -57,6 +57,9 @@ services:
|
|||||||
<<: *common-env
|
<<: *common-env
|
||||||
# ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다.
|
# ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다.
|
||||||
SCHEDULER_ENABLED: "1"
|
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)을 아래 볼륨으로 이 컨테이너 안에 마운트한
|
# ★ GSC_CREDENTIALS_HOST_FILE(호스트 경로, .env)을 아래 볼륨으로 이 컨테이너 안에 마운트한
|
||||||
# 고정 자리다. search_console_settings.load_settings() 가 실제로 읽는 건 이 값이다 —
|
# 고정 자리다. search_console_settings.load_settings() 가 실제로 읽는 건 이 값이다 —
|
||||||
# 호스트 경로를 코드에 그대로 넘기면 컨테이너 안에서 그 경로가 없어 실패한다.
|
# 호스트 경로를 코드에 그대로 넘기면 컨테이너 안에서 그 경로가 없어 실패한다.
|
||||||
|
|||||||
@ -61,6 +61,14 @@
|
|||||||
`scheduler/__init__.py` 에 `add_job` 한 줄, 로직은 `scheduler/jobs.py` → `services/blog_service.py`.
|
`scheduler/__init__.py` 에 `add_job` 한 줄, 로직은 `scheduler/jobs.py` → `services/blog_service.py`.
|
||||||
|
|
||||||
- **주기**: 하루 1회(KST 새벽 04:10). `SCHEDULER_ENABLED=1` 인 프로세스에서만 돈다(이미 그 규약이다)
|
- **주기**: 하루 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) 미만인 업장 —
|
- **대상**: 발행된 사이트 중 재고(DRAFT+REVIEWED)가 `REFILL_BELOW`(30) 미만인 업장 —
|
||||||
하루 한 통씩 나간다고 보면 한 달치를 채우는 셈이다
|
하루 한 통씩 나간다고 보면 한 달치를 채우는 셈이다
|
||||||
- **한 번에 `BATCH_SIZE`(30)건**씩. 앞 회차의 `topic_key` 목록을 프롬프트에 넣어 중복을 막는다
|
- **한 번에 `BATCH_SIZE`(30)건**씩. 앞 회차의 `topic_key` 목록을 프롬프트에 넣어 중복을 막는다
|
||||||
|
|||||||
@ -40,11 +40,18 @@ def start_scheduler():
|
|||||||
id="alert-outbox", max_instances=1, coalesce=True)
|
id="alert-outbox", max_instances=1, coalesce=True)
|
||||||
_scheduler.add_job(sweep_queue_health, "interval", minutes=5,
|
_scheduler.add_job(sweep_queue_health, "interval", minutes=5,
|
||||||
id="queue-health", max_instances=1, coalesce=True)
|
id="queue-health", max_instances=1, coalesce=True)
|
||||||
# 미니 블로그 — 새벽에 재고를 채우고, 아침에 검수 통과분을 보낸다(docs/MINI_BLOG.md).
|
# 미니 블로그 — 아침에 검수 통과분을 보낸다(docs/MINI_BLOG.md).
|
||||||
# ★★ 재고 채우기가 **등록돼 있지 않았다**(실측 2026-09-30). 주석만 "새벽에 채운다" 라고
|
#
|
||||||
# 말하고 `sweep_blog_drafts` 는 어디에서도 불리지 않아, 자동 생성이 한 번도 돈 적이 없다.
|
# ★★ 재고 **자동** 채우기는 기본으로 돌지 않는다(2026-10-01 대표 지시: "이거는 안보내도
|
||||||
# 증상이 "메일이 안 온다" 로만 보여서 발송 쪽을 보게 된다 — 실제로는 **보낼 글이
|
# 될것같아"). 글은 빌더 화면의 [지금 생성하기] 로 사장님이 직접 만든다.
|
||||||
# 0건**이었고, 지금 쌓여 있는 글은 전부 화면의 [지금 생성하기] 로 손으로 만든 것이다.
|
# 켜면 발행된 사이트 **전체**가 대상이고 업장당 최대 30건이라, 첫 회차에 AI 호출이
|
||||||
|
# 한꺼번에 일어난다 — 그 비용을 감수할지는 사람이 정할 일이지 기본값이 정할 일이 아니다.
|
||||||
|
#
|
||||||
|
# ★ 그래서 `add_job` 을 지우지 않고 스위치로 격리했다. 예전에는 **등록 자체가 없었고**
|
||||||
|
# 주석만 "새벽에 채운다" 라고 말했다(실측 2026-09-30) — 그 상태는 "끈 것" 과 "빠뜨린 것"
|
||||||
|
# 이 구별되지 않아, 증상("메일이 안 온다")을 보고 발송 쪽을 뒤지게 된다.
|
||||||
|
# 실제로는 보낼 글이 0건이었다. 지금은 꺼져 있다는 사실이 코드에 남는다.
|
||||||
|
if os.environ.get("BLOG_AUTO_DRAFTS_ENABLED") == "1":
|
||||||
# 발송(09:00)보다 앞서야 그날 아침에 나갈 재고가 있다.
|
# 발송(09:00)보다 앞서야 그날 아침에 나갈 재고가 있다.
|
||||||
_scheduler.add_job(sweep_blog_drafts, "cron", hour=4, minute=10,
|
_scheduler.add_job(sweep_blog_drafts, "cron", hour=4, minute=10,
|
||||||
id="blog-drafts", max_instances=1, coalesce=True)
|
id="blog-drafts", max_instances=1, coalesce=True)
|
||||||
|
|||||||
@ -12,26 +12,40 @@ import scheduler
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def started(monkeypatch):
|
def start(monkeypatch):
|
||||||
|
"""실제로 돌리지 않는다 — 등록만 본다."""
|
||||||
|
def _start(**env):
|
||||||
monkeypatch.setenv("SCHEDULER_ENABLED", "1")
|
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)
|
monkeypatch.setattr("apscheduler.schedulers.asyncio.AsyncIOScheduler.start", lambda self: None)
|
||||||
scheduler._scheduler = None
|
scheduler._scheduler = None
|
||||||
scheduler.start_scheduler()
|
scheduler.start_scheduler()
|
||||||
yield scheduler._scheduler
|
return scheduler._scheduler
|
||||||
|
|
||||||
|
yield _start
|
||||||
scheduler._scheduler = None
|
scheduler._scheduler = None
|
||||||
|
|
||||||
|
|
||||||
def test_미니블로그는_생성과_발송이_둘_다_등록된다(started):
|
def test_발송은_스위치_없이도_등록된다(start):
|
||||||
"""★ 발송만 있으면 보낼 글이 영원히 0건이다."""
|
"""메일·카톡 발송은 항상 돈다 — 사장님이 직접 만든 글도 나가야 한다."""
|
||||||
ids = {j.id for j in started.get_jobs()}
|
assert "blog-mail" in {j.id for j in start().get_jobs()}
|
||||||
assert "blog-drafts" in ids, "재고 채우기가 등록되지 않았다 — 자동 생성이 안 돈다"
|
|
||||||
assert "blog-mail" in ids
|
|
||||||
|
|
||||||
|
|
||||||
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")]))
|
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")
|
assert hour("blog-drafts") < hour("blog-mail")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user