o2o-site-AEO/solution/backend/scheduler/jobs.py
Mina Choi 872d00f3c4 [feat] solution: 미니 블로그·이용후기·예약요청 추가, /s/stay 목업·발행 사이트 UI 다수 수정
인앱 미니 블로그(AI 자동 포스트, 이메일 승인)·이용후기(즉시 게시)·예약 요청(메일 발송)을
새로 붙였고, 병행해서 /s/stay 목업과 발행 사이트 공통 렌더러(UnitsSection·FestivalSection·
LocalGuideSection·WeatherSection 등)의 UI 버그를 다수 고쳤다. 범위가 넓지만 한 주 분량
작업을 한 커밋으로 묶어 달라는 요청에 따라 하나로 묶는다.

- solution/backend: post/review/booking_request 라우터·서비스·CRUD 추가, 스케줄러에
  블로그 초안 생성(새벽 4:10)·발송(아침 9:00) cron 등록, 마이그레이션 4건 추가
- solution/frontend, admin/frontend: 생성된 API 클라이언트 갱신, 리뷰 모더레이션·
  블로그 글 관리 페이지 추가
- solution/site/src: 객실 상세+실시간예약(날짜선택·연락처 폼)을 모달로 통합, 축제·
  주변안내 카드 클릭 시 모달 전환, 후기 목록 카드 UI, 공용 Modal 컴포넌트 신설,
  날씨 문구 동기화 버그 수정(하늘줄·기온줄 한 타이머로), 시설·편의 가능/불가 아이콘
  색상 하이라이트, 헤더 메뉴 순서를 실제 섹션 순서에 맞춤, 하단 탭바 아이콘 정렬 버그
  (line-height) 수정, 추천일정 점선 연결+데스크톱 자동펼침/모바일 축소, 채널 라벨에
  크롤링 원문("NOL")이 새던 것을 bookingLabel() 로 교체
- solution/site/scripts/mockup: /s/stay 패치 스크립트·주입 CSS·JS 다수 수정, stay4~6
  빌드 스크립트 추가(다른 세션 작업)

테스트: solution/site `npx tsc --noEmit` 통과, `npx vitest run` 93 passed,
solution/backend `pytest tests/test_booking_request.py` 6 passed(로컬 DB 대상).
예약 요청 메일은 실제 발송까지 확인(place 66894a1b 소유자 이메일 누락을 DB에서 보정).
2026-09-18 09:03:18 +09:00

79 lines
3.6 KiB
Python

"""스케줄 잡 로직(what). '언제 도느냐'(scheduler/__init__.py)와 분리된, 잡이 실제로 하는 일.
잡은 '대상을 고르는 것'까지만 하고, 실제 처리는 도메인 service 가 책임진다.
(지역정보 갱신 · 수집 재시도 · 개별 사이트 재빌드가 여기로 들어온다.)
"""
from common.logger import LOG
async def sweep_alert_outbox():
"""대기 중인 알림을 실제로 보낸다(services/alert_service.process_outbox)."""
from services import alert_service
try:
await alert_service.process_outbox()
except Exception as ex: # noqa: BLE001 — 스윕 실패가 스케줄러를 죽이면 안 된다(다음 주기 재시도)
LOG.w(f"[scheduler] 알림 발송 스윕 실패: {type(ex).__name__}: {ex}")
async def sweep_queue_health():
"""잡 큐가 막혔는지 주기적으로 본다 — dead-letter 누적·좀비 실행·오래 밀린 PENDING.
★ 왜 필요한가: 개별 잡의 DEAD 전이는 worker/runner.py 가 그 자리에서 바로 알린다. 이건
그것과 다른 신호다 — 잡 하나하나는 재시도 중(아직 DEAD 아님)인데 **큐 전체가 정체**된
경우(워커 프로세스가 죽었거나 DB 순단이 길어지는 경우)는 개별 잡 알림만으로는 안 보인다.
★ 복구되면 한 번만 알린다 — send_alert/resolve_alert 의 dedupe_key 가 그 판단을 한다."""
from crud.job_crud import JobQueue
from services import alert_service
try:
snap = await JobQueue().ops()
except Exception as ex: # noqa: BLE001
LOG.w(f"[scheduler] 큐 상태 조회 실패: {type(ex).__name__}: {ex}")
return
# 기준값: dead-letter 가 최근 1시간에 쌓였거나, 좀비 실행이 있거나, 가장 오래된 PENDING 이
# 30분 넘게 안 집혔다(정상 워커라면 대기 잡을 몇 초 안에 claim 한다).
problems = []
if snap.get("dead_1h", 0) > 0:
problems.append(f"최근 1시간 dead-letter {snap['dead_1h']}건")
if snap.get("stuck_running", 0) > 0:
problems.append(f"좀비 실행 {snap['stuck_running']}건(lease 만료 또는 10분 초과)")
if snap.get("oldest_pending_sec", 0) > 1800:
problems.append(f"가장 오래된 대기 잡이 {snap['oldest_pending_sec'] // 60}분째 안 집힘")
dedupe_key = "queue_health"
if problems:
await alert_service.send_alert(
kind="queue_stuck",
title="잡 큐 정체",
detail=" · ".join(problems) + f"\n{snap}",
dedupe_key=dedupe_key,
)
else:
await alert_service.resolve_alert(dedupe_key, "잡 큐 정상으로 돌아옴")
async def sweep_blog_drafts():
"""미니 블로그 재고 채우기(services/blog_jobs.generate_drafts)."""
from services import blog_jobs
try:
made = await blog_jobs.generate_drafts()
if made:
LOG.i(f"[scheduler] 미니 블로그 초안 {made}건 생성")
except Exception as ex: # noqa: BLE001 — 생성 실패가 스케줄러를 죽이면 안 된다
LOG.w(f"[scheduler] 미니 블로그 생성 실패: {type(ex).__name__}: {ex}")
async def sweep_blog_mail():
"""검수를 통과한 글을 사장님에게 보낸다(services/blog_jobs.send_reviewed)."""
from services import blog_jobs
try:
sent = await blog_jobs.send_reviewed()
if sent:
LOG.i(f"[scheduler] 미니 블로그 메일 {sent}통 발송")
except Exception as ex: # noqa: BLE001
LOG.w(f"[scheduler] 미니 블로그 발송 실패: {type(ex).__name__}: {ex}")