diff --git a/docs/ALERTS.md b/docs/ALERTS.md index 11aa227..f062796 100644 --- a/docs/ALERTS.md +++ b/docs/ALERTS.md @@ -13,7 +13,7 @@ | `partial_failure` | 노래 등 곁가지 생성 실패(발행 자체는 계속) | `song_failed:{place_id}` | | `queue_stuck` | dead-letter 누적·좀비 실행·PENDING 30분 이상 정체 | `queue_health` | | `recovery` | 위 dedupe_key 가 다음 정상 상태에서 풀릴 때 한 번 | 없음(매번 새 행) | -| `activity` | 수집 시작·채널 실패·완료 · 사진 분석 · 첫 발행/재발행/빌드/되돌리기 시작·끝(`services/activity_feed.py`) | 없음(매번 보낸다) | +| `activity` | 채널 크롤링 실패 · 사진 분석 일부 실패(`services/activity_feed.py`) | 없음(매번 보낸다) | ★ **게이트 반려는 알리지 않는다.** 사장님이 fact 를 안 채웠거나 고유 콘텐츠가 없어서 막힌 건 운영자가 손댈 일이 아니다 — `build_service._fail(reason, gate=None)` 일 때만 `build_failed`. diff --git a/solution/backend/services/build_service.py b/solution/backend/services/build_service.py index e064306..f209d02 100644 --- a/solution/backend/services/build_service.py +++ b/solution/backend/services/build_service.py @@ -1,6 +1,5 @@ """정적 빌드 + 발행 — BUILD 잡이 하는 일.""" import os -import time import uuid from sqlalchemy import select @@ -26,7 +25,6 @@ from crud.site_crud import SiteCRUD from crud.place_crud import PlaceCRUD from crud.post_crud import PostCRUD from services import ( - activity_feed, alert_service, azure_static, indexnow, @@ -120,45 +118,6 @@ async def _log(site_id, version_id, action: PublishAction, result: PublishResult async def run_build(job: dict) -> dict: """BUILD 잡 핸들러.""" - started = time.monotonic() - try: - result = await _run_build(job) - except Exception as ex: - await activity_feed.post( - "❌ 빌드 실패", - [f"place_id={job['payload'].get('place_id')}", f"{type(ex).__name__}: {ex}", - f"{time.monotonic() - started:.0f}초"], - ) - raise - await post_build_summary(result, time.monotonic() - started) - return result - - -async def post_build_summary(result: dict, elapsed: float) -> None: - version = f"v{result.get('version')}" - mode = result.get("mode") or "빌드" - lines = [f"{result.get('label') or result.get('place_id')} {version}"] - if result.get("build_status") == "BUILT": - title = f"✅ {mode} 완료" - if result.get("url"): - lines.append(result["url"]) - elif result.get("gate", {}).get("passed") is False: - title = f"🚫 {mode} 반려(게이트)" - lines.append(str(result.get("error") or "")) - else: - title = f"❌ {mode} 실패" - lines.append(str(result.get("error") or "")) - media = result.get("render_media") - if media: - lines.append( - f"굽기 — 사진 {media.get('total', 0)}장: 새로 받음 {media.get('fetched', 0)} · " - f"기존 {media.get('cached', 0)} · 실패 {media.get('failed', 0)}" - ) - lines.append(f"{elapsed:.0f}초") - await activity_feed.post(title, lines) - - -async def _run_build(job: dict) -> dict: payload = job["payload"] place_id = payload["place_id"] owner_user_id = payload["owner_user_id"] @@ -173,7 +132,6 @@ async def _run_build(job: dict) -> dict: raise BuildAborted(f"사업장을 찾을 수 없다: {place_id}") site = await ensure_site(place_id) - mode = ("재발행" if site.published_at is not None else "첫 발행") if want_publish else "빌드만" # 주변 정보(맛집·관광지·축제·코스)는 빌드 시점에 업장 좌표로 새로 받는다 — 발행본은 정적이라 이때 받은 값이 실린다. try: @@ -240,12 +198,7 @@ async def _run_build(job: dict) -> dict: raise BuildAborted(f"버전 생성 실패: {add_err.name}") result = {"place_id": place_id, "site_id": str(site.site_id), "version": version_no, - "site_version_id": str(version.site_version_id), "label": activity_feed.place_label(place), - "mode": mode} - await activity_feed.post( - f"🏗 {mode} 시작", - [f"{result['label']} v{version_no}", f"템플릿 {site.template_id or '기본'}"], - ) + "site_version_id": str(version.site_version_id)} # 잡 결과에 남긴다 — "노래가 왜 없나" 를 잡 하나만 열어 보면 알 수 있어야 한다. if song_result is not None: result["song"] = song_result @@ -314,8 +267,6 @@ async def _run_build(job: dict) -> dict: # 발행하지 않는다. return await _fail(str(ex)) - if report.get("media"): - result["render_media"] = report["media"] mismatches = list(report.get("mismatches") or []) unique_count_raw = report.get("uniqueContentCount") unique_count = unique_count_raw or 0 @@ -415,7 +366,6 @@ async def _run_build(job: dict) -> dict: if thumbnail_url: site.thumbnail_url = thumbnail_url result["published"] = True - result["url"] = site_payload.publish_url(place, site) LOG.i(f"[build] place={place_id} v{version_no} 발행 완료 " f"(고유 콘텐츠 {unique_count}건 · {report.get('routes')} 페이지)") else: diff --git a/solution/backend/services/collect_service.py b/solution/backend/services/collect_service.py index 38d8705..a4f7b76 100644 --- a/solution/backend/services/collect_service.py +++ b/solution/backend/services/collect_service.py @@ -458,47 +458,14 @@ async def store_media(place_id: str, sources: list, unit_map: dict) -> dict: # 오케스트레이션 async def run_collect(job: dict) -> dict: """COLLECT 잡 핸들러.""" - started = time.monotonic() with collect_diagnostics.collecting(): - try: - result = await _run_collect(job) - except Exception as ex: - await activity_feed.post( - "❌ 수집 실패", - [f"place_id={job['payload'].get('place_id')}", f"{type(ex).__name__}: {ex}", - f"{time.monotonic() - started:.0f}초"], - ) - raise + result = await _run_collect(job) issues = collect_diagnostics.snapshot() if issues: result["issues"] = issues - await _post_collect_summary(result, issues, time.monotonic() - started) return result -async def _post_collect_summary(result: dict, issues: list[dict], elapsed: float) -> None: - lines = [result.get("label") or f"place_id={result.get('place_id')}"] - if result.get("note"): - lines.append(result["note"]) - for row in result.get("channels") or []: - lines.append(row) - facts = result.get("facts") - if facts: - reasons = ", ".join(f"{k} {v}" for k, v in facts["by_reason"].items()) - lines.append(f"fact 저장 {facts['stored']} · 반려 {facts['rejected']}" + (f" ({reasons})" if reasons else "")) - media = result.get("media") - if media: - lines.append(f"사진 {media['stored']}장 (중복 {media['skipped_duplicate']})") - cov = result.get("coverage") - if cov and cov["missing"]: - lines.append(f"필수 항목 {cov['covered']}/{cov['total']} — 누락 {', '.join(cov['missing'])}") - other = [i for i in issues if i["stage"] != "fetch"] - for issue in other: - lines.append(f"⚠️ {issue['stage']} 실패: {issue['error_type']}: {issue['message'][:200]}") - lines.append(f"{elapsed:.0f}초") - await activity_feed.post("✅ 수집 완료", lines) - - async def _run_collect(job: dict) -> dict: payload = job["payload"] place_id = payload["place_id"] @@ -528,12 +495,6 @@ async def _run_collect(job: dict) -> dict: link.channel == LinkChannel.NAVER_PLACE.value for link in targets ) - await activity_feed.post( - "🔎 수집 시작", - [result["label"], f"place_id={place_id}", - f"크롤링 대상 {len(targets)}건: " + (", ".join(activity_feed.channel_name(l.channel) for l in targets) or "없음")], - ) - if not targets: result["note"] = "크롤링 대상이 없다(어댑터가 처리할 수 있는 확정 URL 0건)" await _finish(place_id, owner_user_id, PlaceStatus.REVIEW) diff --git a/solution/backend/services/rollback_service.py b/solution/backend/services/rollback_service.py index b2bced9..c2553c5 100644 --- a/solution/backend/services/rollback_service.py +++ b/solution/backend/services/rollback_service.py @@ -1,6 +1,5 @@ """대상 버전 스냅샷 로드 → payload 재조립 → 렌더 → 게이트 → 공개 주소 전환""" import os -import time import uuid from common.database.db_session_manager import DB_SESSION_MNG @@ -18,8 +17,8 @@ from common.logger import LOG from common.utils.gtime import GTime from crud.place_crud import PlaceCRUD from crud.site_crud import SiteCRUD -from services import activity_feed, alert_service, azure_static, indexnow, publish_gate, render_service, site_payload -from services.build_service import post_build_summary, ensure_site, load_channel_links +from services import alert_service, azure_static, indexnow, publish_gate, render_service, site_payload +from services.build_service import ensure_site, load_channel_links _site_crud = SiteCRUD() _place_crud = PlaceCRUD() @@ -46,25 +45,6 @@ async def _log(site_id, version_id, result: PublishResult, gate=None, actor=None async def run_rollback(job: dict) -> dict: """ROLLBACK 잡 핸들러.""" - started = time.monotonic() - try: - result = await _run_rollback(job) - except Exception as ex: - await activity_feed.post( - "❌ 되돌리기 실패", - [f"place_id={job['payload'].get('place_id')} → v{job['payload'].get('target_version')}", - f"{type(ex).__name__}: {ex}", f"{time.monotonic() - started:.0f}초"], - ) - raise - await post_build_summary( - {**result, "version": result.get("target_version"), "mode": "되돌리기", - "build_status": "BUILT" if result.get("rolled_back") else "FAILED"}, - time.monotonic() - started, - ) - return result - - -async def _run_rollback(job: dict) -> dict: payload = job["payload"] place_id = payload["place_id"] owner_user_id = payload["owner_user_id"] @@ -96,9 +76,7 @@ async def _run_rollback(job: dict) -> dict: result = { "place_id": place_id, "site_id": str(site.site_id), "target_version": target_version, "site_version_id": str(version.site_version_id), - "label": activity_feed.place_label(place), } - await activity_feed.post("🏗 되돌리기 시작", [f"{result['label']} → v{target_version}"]) async def _fail(reason: str, gate: publish_gate.GateResult | None = None): await _log(site.site_id, version.site_version_id, PublishResult.REJECTED if gate else PublishResult.FAILED, @@ -129,8 +107,6 @@ async def _run_rollback(job: dict) -> dict: except render_service.RenderFailed as ex: return await _fail(str(ex)) - if report.get("media"): - result["render_media"] = report["media"] mismatches = list(report.get("mismatches") or []) unique_count_raw = report.get("uniqueContentCount") @@ -165,6 +141,5 @@ async def _run_rollback(job: dict) -> dict: await alert_service.resolve_alert(f"build_failed:{place_id}", f"롤백 성공 — {place_id} → v{target_version}") result["rolled_back"] = True - result["url"] = site_payload.publish_url(place, site) LOG.i(f"[rollback] place={place_id} v{target_version} 로 되돌림") return result diff --git a/solution/backend/services/vision_service.py b/solution/backend/services/vision_service.py index 284e868..0eb7b06 100644 --- a/solution/backend/services/vision_service.py +++ b/solution/backend/services/vision_service.py @@ -113,9 +113,10 @@ async def run_vision(job: dict) -> dict: LOG.i(f"[vision] place={place_id} {stat['analyzed']}장 → 자동반영 {stat['approved']} · " f"확인필요 {stat['needs_review']} · 실패 {stat['failed']} (임계값 {threshold})") - await activity_feed.post( - "🖼 사진 분석 완료" if not stat["failed"] else "⚠️ 사진 분석 일부 실패", - [activity_feed.place_label(place), - f"분석 {stat['analyzed']} · 승인 {stat['approved']} · 확인대기 {stat['needs_review']} · 실패 {stat['failed']}"], - ) + if stat["failed"]: + await activity_feed.post( + "⚠️ 사진 분석 일부 실패", + [activity_feed.place_label(place), + f"분석 {stat['analyzed']} · 승인 {stat['approved']} · 확인대기 {stat['needs_review']} · 실패 {stat['failed']}"], + ) return stat