수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
452 lines
21 KiB
Python
452 lines
21 KiB
Python
from enum import Enum, auto
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
class CodeEnum(Enum):
|
|
"""OpenAPI 스키마에 x-enum-varnames(멤버 이름)을 실어 orval 이 이름 있는 enum 을 생성하게 하는 베이스."""
|
|
|
|
@classmethod
|
|
def __get_pydantic_json_schema__(cls, core_schema, handler):
|
|
json_schema = handler(core_schema)
|
|
json_schema = handler.resolve_ref_schema(json_schema)
|
|
json_schema["x-enum-varnames"] = [m.name for m in cls]
|
|
return json_schema
|
|
|
|
|
|
class ErrorType(Enum):
|
|
"""서버 전역 결과 코드. Res_WebPacketProtocol.result 에 담겨 클라이언트로 전달된다.
|
|
HTTP status 와 겹치지 않도록 구간을 분리해서 관리한다.
|
|
|
|
도메인 코드는 모듈이 붙을 때 구간을 새로 열어 추가한다
|
|
(places 1200 / facts 1300 / collector 1400 / generator 1500 / local 1600 / sites 1700 / reports 1800 예약).
|
|
"""
|
|
|
|
SUCCESS = 0
|
|
FAIL = 1
|
|
|
|
# DB / Redis 에러
|
|
DB_RUN_FAILED = 10
|
|
DB_ALREADY_SAME_KEY = auto()
|
|
DB_INVALID_KEY = auto()
|
|
DB_EMPTY_DATA = auto()
|
|
DB_INVALID_TYPE = auto()
|
|
|
|
# 요청/직렬화 에러
|
|
JSON_PARSE_ERROR = 100
|
|
INVALID_REQUEST_DATA = auto()
|
|
INTERNAL_EXCEPTION = auto()
|
|
|
|
# http 에러 코드와 겹치지 않게 설정 - router 전용 예외 발생 옵션
|
|
HTTP_FORBIDDEN = 403
|
|
HTTP_INVALID_CLIENT_REQUEST = 419
|
|
HTTP_TO_MANY_REQUEST = 429
|
|
HTTP_INVALID_CLIENT_ACCESS = 433
|
|
HTTP_ACCESS_TOKEN_EXPIRED = 434
|
|
HTTP_REFRESH_TOKEN_EXPIRED = 435
|
|
HTTP_INVALID_TOKEN_ACCESS = 436
|
|
|
|
# 계정 관련 에러
|
|
ACCOUNT_INVALID_INFO = 1100
|
|
ACCOUNT_ALREADY_EXIST = auto()
|
|
ACCOUNT_BLOCKED_USER = auto()
|
|
ACCOUNT_NOT_FOUND = auto()
|
|
ACCOUNT_FORBIDDEN = auto() # 최고관리자 외 접근 / 다른 회사·최고관리자 대상 변경 시도
|
|
ACCOUNT_PROVIDER_CONFLICT = auto() # 이미 다른 로그인 수단으로 가입된 이메일 — 자동 연결하지 않는다(DECISIONS 1절)
|
|
OAUTH_NOT_CONFIGURED = auto() # GOOGLE_CLIENT_ID 미설정 — 구글 로그인만 꺼진다
|
|
OAUTH_INVALID_TOKEN = auto() # 구글 ID 토큰 서명·수신자·만료 검증 실패
|
|
|
|
# 사업장(places) 관련 에러
|
|
PLACE_NOT_FOUND = 1200
|
|
PLACE_ALREADY_EXIST = auto() # 같은 회사 안에 같은 카카오 장소 ID — 중복 등록
|
|
PLACE_NOT_VERIFIED = auto() # ★ 동일 업소 검증 전 — 수집·발행 진입 금지
|
|
PLACE_VERIFY_NO_CANDIDATE = auto() # 카카오 로컬에서 후보를 못 찾음
|
|
PLACE_VERIFY_AMBIGUOUS = auto() # 동명 업소 다수 — 사람이 골라야 함
|
|
PLACE_INVALID_CATEGORY = auto() # 지원하지 않는 업종 코드
|
|
UNIT_NOT_FOUND = auto()
|
|
LINK_NOT_FOUND = auto()
|
|
LINK_NOT_CONFIRMED = auto() # ★ 확정 안 된 URL — 크롤링 대상 아님
|
|
MEDIA_NOT_FOUND = auto()
|
|
|
|
# fact 관련 에러
|
|
FACT_NOT_FOUND = 1300
|
|
FACT_INVALID_KEY = auto() # 업종 스키마에 없는 key
|
|
FACT_INVALID_TRANSITION = auto() # 허용되지 않은 검증 상태 전이
|
|
FACT_LOCKED = auto() # ★ CORRECTED(사장님 수정본) — 자동 갱신이 덮어쓸 수 없다
|
|
FACT_SOURCE_REQUIRED = auto() # source_type 이 owner 가 아닌데 source_url 이 없음
|
|
FAQ_NOT_FOUND = auto()
|
|
FAQ_UNGROUNDED = auto() # ★ 확보된 fact 로 뒷받침되지 않는 문장 — 반려
|
|
|
|
# 수집(collector) 관련 에러
|
|
COLLECT_ADAPTER_NOT_FOUND = 1400 # 해당 URL 을 처리할 어댑터 없음
|
|
COLLECT_ADAPTER_DISABLED = auto() # 법무 검토 전이라 미등록된 어댑터(HeadlessAdapter 등)
|
|
COLLECT_FETCH_FAILED = auto()
|
|
COLLECT_ALREADY_RUNNING = auto()
|
|
|
|
# 생성(generator) 관련 에러
|
|
GENERATOR_NOT_CONFIGURED = 1500 # GEMINI_API_KEY 미설정
|
|
GENERATOR_CALL_FAILED = auto()
|
|
GENERATOR_INVALID_OUTPUT = auto() # 구조화 출력 파싱 실패
|
|
GENERATOR_LOW_CONFIDENCE = auto() # 신뢰도 낮음 — 자동 반영 금지, 사람 확인 큐로
|
|
|
|
# 지역 정보(local) 관련 에러
|
|
LOCAL_NOT_CONFIGURED = 1600 # KAKAO_REST_API_KEY / TOUR_API_KEY 미설정
|
|
LOCAL_REGION_UNKNOWN = auto() # 좌표 → 행정구역 코드 변환 실패
|
|
LOCAL_FETCH_FAILED = auto() # ★ 실패해도 직전 값을 유지한다 — 빈 값을 내보내지 않는다
|
|
|
|
# 사이트(sites) 관련 에러
|
|
SITE_NOT_FOUND = 1700
|
|
SITE_VERSION_NOT_FOUND = auto()
|
|
SITE_BUILD_FAILED = auto()
|
|
PUBLISH_UNVERIFIED_FACT = auto() # ★ 미검증 fact 포함 — 발행 거부
|
|
PUBLISH_NO_UNIQUE_CONTENT = auto() # ★ 고유 콘텐츠 0건 — 발행 거부(스팸 판정 대상)
|
|
PUBLISH_JSONLD_MISMATCH = auto() # ★ 구조화 데이터 값 != 화면 값 — 빌드 실패
|
|
PUBLISH_REQUIRED_FACT_MISSING = auto() # 업종 스키마의 required 필드 누락
|
|
SITE_SLUG_LOCKED = auto() # ★ 이미 발행된 사이트의 주소 변경 — 색인된 페이지가 404 가 된다
|
|
|
|
# 리포트(reports) 관련 에러
|
|
REPORT_NOT_FOUND = 1800
|
|
|
|
# 작업 큐(job) 관련 에러
|
|
JOB_NOT_FOUND = 1900
|
|
JOB_ALREADY_QUEUED = auto() # 같은 dedupe_key 의 활성 잡이 이미 있다
|
|
JOB_NOT_DEAD = auto() # DEAD 가 아닌 잡을 재큐하려 함
|
|
|
|
|
|
# ErrorType 의 HTTP_* 값과 status_code 를 맞춰 router 단에서 raise 한다.
|
|
EXCEPTION_FORBIDDEN = HTTPException(status_code=ErrorType.HTTP_FORBIDDEN.value, detail=ErrorType.HTTP_FORBIDDEN.name)
|
|
EXCEPTION_INVALID_CLIENT_REQUEST = HTTPException(status_code=ErrorType.HTTP_INVALID_CLIENT_REQUEST.value, detail=ErrorType.HTTP_INVALID_CLIENT_REQUEST.name)
|
|
EXCEPTION_TO_MANY_REQUEST = HTTPException(status_code=ErrorType.HTTP_TO_MANY_REQUEST.value, detail=ErrorType.HTTP_TO_MANY_REQUEST.name)
|
|
EXCEPTION_INVALID_CLIENT_ACCESS = HTTPException(status_code=ErrorType.HTTP_INVALID_CLIENT_ACCESS.value, detail=ErrorType.HTTP_INVALID_CLIENT_ACCESS.name)
|
|
EXCEPTION_ACCESS_TOKEN_EXPIRED = HTTPException(status_code=ErrorType.HTTP_ACCESS_TOKEN_EXPIRED.value, detail=ErrorType.HTTP_ACCESS_TOKEN_EXPIRED.name)
|
|
EXCEPTION_REFRESH_TOKEN_EXPIRED = HTTPException(status_code=ErrorType.HTTP_REFRESH_TOKEN_EXPIRED.value, detail=ErrorType.HTTP_REFRESH_TOKEN_EXPIRED.name)
|
|
EXCEPTION_HTTP_INVALID_TOKEN_ACCESS = HTTPException(status_code=ErrorType.HTTP_INVALID_TOKEN_ACCESS.value, detail=ErrorType.HTTP_INVALID_TOKEN_ACCESS.name)
|
|
|
|
|
|
class DBType(Enum):
|
|
"""논리 DB 구분. 모델마다 DBType() 으로 자신이 속한 DB 를 반환한다.
|
|
DB 가 늘어나면 여기에 추가하고 db_session_manager 의 맵에 등록만 하면 된다.
|
|
"""
|
|
|
|
MAIN = 1
|
|
|
|
|
|
class DBWRType(Enum):
|
|
"""Read / Write 접속 구분. 조회는 DB_READ, 변경은 DB_WRITE 엔진을 사용한다."""
|
|
|
|
DB_READ = 1
|
|
DB_WRITE = 2
|
|
|
|
|
|
# 도메인 코드값
|
|
class UserStatus(CodeEnum):
|
|
"""users.status 코드값."""
|
|
|
|
ACTIVE = 1
|
|
INACTIVE = 2
|
|
|
|
|
|
class UserRole(CodeEnum):
|
|
"""users.role 코드값.
|
|
1=일반, 2=최고관리자(고객사 최상위), 3=개발자(우리 내부 운영 계정).
|
|
개발자 계정은 고객사에 존재를 노출하지 않는다 — 회원 목록에서 빼고 총계에도 넣지 않는다."""
|
|
|
|
USER = 1
|
|
OWNER = 2 # 최고관리자: 자기 회사 계정 관리 + 회사 설정
|
|
DEVELOPER = 3 # 개발자(내부 운영): 최고관리자 권한 전부 + 고객사에 보이지 않음
|
|
|
|
|
|
class AuthProvider(CodeEnum):
|
|
"""users.provider 코드값. 이 계정이 무엇으로 신원을 증명하는가.
|
|
|
|
한 계정은 수단 하나다 — 같은 이메일이라도 id/pw 계정과 구글 계정을 자동으로 잇지 않는다.
|
|
이으려면 "먼저 가입한 쪽의 소유"를 증명받아야 하는데, 그 증명 없이 이메일만 보고 이으면
|
|
남이 먼저 만들어 둔 계정에 내 구글 로그인이 들어간다(계정 선점). 보류 사유는 DECISIONS.md 1절."""
|
|
|
|
LOCAL = 1 # id/pw
|
|
GOOGLE = 2 # 구글 ID 토큰
|
|
|
|
|
|
class CompanyStatus(CodeEnum):
|
|
"""companies.status 코드값."""
|
|
|
|
ACTIVE = 1
|
|
INACTIVE = 2
|
|
|
|
|
|
class PlaceCategory(CodeEnum):
|
|
"""places.category 코드값. 업종 — 스키마 파일(common/category_schema/resources/*.json)과 1:1.
|
|
업종 추가 = 여기에 코드 추가 + 스키마 파일 1개 추가."""
|
|
|
|
LODGING = 1 # 숙박
|
|
CAFE = 2 # 카페
|
|
RESTAURANT = 3 # 음식점
|
|
CLINIC = 4 # 피부과·성형외과
|
|
|
|
|
|
class ExternalPlaceSource(CodeEnum):
|
|
"""places.external_source 코드값. 동일 업소 검증에 쓴 외부 장소 DB.
|
|
|
|
카카오는 안정적인 고유 place id 를 준다 → 그걸로 중복 등록을 막는다.
|
|
네이버는 고유 id 가 없다(응답의 link 는 업체 홈페이지다) → 상호명+도로명주소로 막는다."""
|
|
|
|
KAKAO = 1 # dapi.kakao.com — 고유 place id O · 전화번호 O · 행정구역 코드 O
|
|
NAVER = 2 # openapi.naver.com 지역검색 — 고유 id X · 전화번호 X · 5건 제한
|
|
|
|
|
|
class PlaceStatus(CodeEnum):
|
|
"""places.status 코드값. 사업장 생애주기.
|
|
해지는 삭제가 아니라 SUSPENDED 로의 상태 전이다(색인된 페이지를 갑자기 404 로 만들지 않는다)."""
|
|
|
|
DRAFT = 1 # 등록만 됨 — 동일 업소 검증 전
|
|
COLLECTING = 2 # 수집 진행 중
|
|
REVIEW = 3 # 검수 대기(사람 확인 큐)
|
|
PUBLISHED = 4 # 발행됨
|
|
SUSPENDED = 5 # 해지·중지 — 사이트 상태 전이로 처리
|
|
|
|
|
|
class SourceType(CodeEnum):
|
|
"""facts.source_type / media.source_type / place_aliases.source_type 공용 코드값.
|
|
값이 어디서 왔는지 — 모든 사실은 출처를 갖는다."""
|
|
|
|
OWNER = 1 # 사장님이 직접 입력·업로드
|
|
API = 2 # 공식 API (카카오 로컬 · TourAPI · Open-Meteo · Perplexity)
|
|
CRAWL = 3 # 크롤링
|
|
LLM = 4 # LLM 생성 — ★ 사실이 아니라 문장에만 쓴다
|
|
|
|
|
|
class FactStatus(CodeEnum):
|
|
"""facts.status / faqs.status / routes.status 공용 검증 상태.
|
|
★ VERIFIED 와 CORRECTED 만 사이트에 노출한다(PUBLISHABLE_FACT_STATUSES)."""
|
|
|
|
UNVERIFIED = 1 # 수집됐으나 아무도 확인 안 함
|
|
PENDING_OWNER = 2 # 사장님 확인 대기
|
|
VERIFIED = 3 # 확인됨 — 노출 가능
|
|
CORRECTED = 4 # 사장님이 고침 — 노출 가능 + ★ 잠김(자동 갱신이 덮어쓸 수 없다)
|
|
REJECTED = 5 # 틀린 값으로 판정 — 노출 안 함
|
|
EXPIRED = 6 # 유효기간 지남 — 노출 안 함, 재수집 대상
|
|
|
|
|
|
# ★ 절대규칙 1: 이 두 상태만 사이트에 노출한다. 발행 게이트가 이 집합으로 필터링한다.
|
|
PUBLISHABLE_FACT_STATUSES = {FactStatus.VERIFIED, FactStatus.CORRECTED}
|
|
|
|
# 후보 — 재수집이 올려놓은 확인 대기 항목. 노출값과 달리 (place, unit, key) 당 여러 건 공존한다.
|
|
CANDIDATE_FACT_STATUSES = {FactStatus.UNVERIFIED, FactStatus.PENDING_OWNER}
|
|
|
|
# ★ 절대규칙 6: 자동 수집(api/crawl/llm)이 덮어쓸 수 없는 상태. 사장님 수정본은 후보로만 도전받는다.
|
|
LOCKED_FACT_STATUSES = {FactStatus.CORRECTED}
|
|
|
|
|
|
class FactWriteOutcome(CodeEnum):
|
|
"""fact 기록 결과. 재수집(업데이트)이 무엇을 했는지 호출측이 알아야 한다 —
|
|
특히 사이트 재빌드가 필요한 경우(PUBLISHED_REPLACED)를 구분해야 한다."""
|
|
|
|
PUBLISHED_CREATED = 1 # 노출값이 없던 자리에 사람이 직접 넣어 바로 노출됐다
|
|
PUBLISHED_REPLACED = 2 # ★ 노출값이 교체됐다 — 사이트 재빌드 대상
|
|
REFRESHED = 3 # 재수집했는데 값이 그대로 — 검증 유지, 확인 시각만 갱신
|
|
CANDIDATE_CREATED = 4 # 노출값과 다른 값이 들어와 후보로 쌓였다(사람 확인 대기)
|
|
CANDIDATE_UPDATED = 5 # 같은 출처의 기존 후보를 새 수집값으로 갱신했다
|
|
|
|
# 검증 상태 전이 허용표. 여기에 없는 전이는 FACT_INVALID_TRANSITION 으로 거부한다.
|
|
FACT_STATUS_TRANSITIONS = {
|
|
FactStatus.UNVERIFIED: {FactStatus.PENDING_OWNER, FactStatus.VERIFIED, FactStatus.REJECTED, FactStatus.EXPIRED},
|
|
FactStatus.PENDING_OWNER: {FactStatus.VERIFIED, FactStatus.CORRECTED, FactStatus.REJECTED, FactStatus.EXPIRED},
|
|
FactStatus.VERIFIED: {FactStatus.CORRECTED, FactStatus.REJECTED, FactStatus.EXPIRED},
|
|
FactStatus.CORRECTED: {FactStatus.CORRECTED, FactStatus.REJECTED}, # 사장님만 다시 고칠 수 있다
|
|
FactStatus.REJECTED: {FactStatus.UNVERIFIED}, # 재수집으로만 되살아난다
|
|
FactStatus.EXPIRED: {FactStatus.UNVERIFIED},
|
|
}
|
|
|
|
|
|
class LinkChannel(CodeEnum):
|
|
"""place_channels.channel 코드값. Perplexity 가 발견하는 채널 종류."""
|
|
|
|
YANOLJA = 1 # 야놀자
|
|
GOODCHOICE = 2 # 여기어때
|
|
NAVER_PLACE = 3 # 네이버 플레이스
|
|
INSTAGRAM = 4
|
|
OFFICIAL_SITE = 5 # 사장님 자체 홈페이지
|
|
BLOG = 6
|
|
# ★ 플레이스와 가른 이유: 이건 **예약 화면 그 자체**다.
|
|
# 플레이스 홈은 예약 버튼을 한 번 더 눌러야 하고, 자동 발견이 검색 URL 을 물어온
|
|
# 경우에는 아예 검색 결과가 뜬다 — 발행본의 "예약" 버튼이 그리로 가면 손님은
|
|
# 예약을 포기한다. 주소는 지어내지 않는다: 플레이스 응답의 naverBookingUrl 그대로다.
|
|
NAVER_BOOKING = 7 # 네이버 예약(m.booking.naver.com)
|
|
ETC = 99
|
|
|
|
|
|
class MediaStatus(CodeEnum):
|
|
"""media.status 코드값. 비전 결과 신뢰도가 낮으면 자동 반영하지 않고 PENDING_REVIEW 로 둔다."""
|
|
|
|
PENDING_REVIEW = 1 # 사람 확인 큐 — Vision 분석 전이거나 신뢰도가 낮다
|
|
APPROVED = 2 # 사람이 확인함(또는 Vision 신뢰도가 충분히 높음)
|
|
REJECTED = 3
|
|
|
|
|
|
class SongStatus(CodeEnum):
|
|
"""place_songs.status 코드값.
|
|
|
|
★ fact·사진과 달리 검증 상태가 없다. 노래는 수집한 사실이 아니라 우리가 만든 창작물이라
|
|
"맞는가" 를 물을 대상이 아니다. 물을 것은 "만들어졌는가" 하나다.
|
|
★ 사이트에는 READY 만 나간다 — 생성 중인 곡을 실으면 재생 버튼이 없는 파일을 가리킨다."""
|
|
|
|
GENERATING = 1 # Suno 가 작곡 중(또는 파일을 아직 못 받았다)
|
|
READY = 2 # 파일까지 받아 뒀다 — 사이트에 나간다
|
|
FAILED = 3 # 생성 실패. 발행은 그대로 진행된다(노래만 없다)
|
|
|
|
|
|
# ★ Vision 결과를 자동 반영해도 되는 신뢰도 하한. 이 아래는 사람 확인 큐(PENDING_REVIEW)로 남긴다.
|
|
# "신뢰도 낮은 항목은 자동 반영하지 말고 사람 확인 큐로 보낸다" 를 한 곳에서만 판단한다.
|
|
VISION_AUTO_APPROVE_CONFIDENCE = 0.7
|
|
|
|
|
|
class LocalContentType(CodeEnum):
|
|
"""local_contents.content_type 코드값. 행정구역 코드 단위로 캐싱되는 지역 정보 종류."""
|
|
|
|
WEATHER = 1 # 날씨 (Open-Meteo) — local_contents(지역 캐시)
|
|
# ↓ 2~5 는 place_contents(업장 반경 캐시). TourAPI locationBasedList2 contentTypeId 와 짝: 15·12·39·25
|
|
FESTIVAL = 2 # 축제/공연/행사 (15)
|
|
ATTRACTION = 3 # 관광지 (12)
|
|
RESTAURANT = 4 # 음식점 (39)
|
|
COURSE = 5 # 여행코스 (25) — 백엔드만. 렌더러 자리는 아직 없다
|
|
# ★ 지역 이야기(가요·일력·인물·연표·엽서·퀴즈). 위 넷과 달리 **좌표가 아니라 행정구역**에 붙는다 —
|
|
# 군산 이야기는 군산 숙소가 같이 쓴다. 여섯을 한 코드로 두고 `area_contents.kind` 로 가르는 이유는,
|
|
# 종류마다 코드를 주면 종류가 늘 때마다 enum·상한표·읽는 쪽이 함께 늘기 때문이다.
|
|
STORY = 6
|
|
|
|
|
|
# 코드값 ↔ **타입명**. `area_contents.kind` 와 `site_sections.data.items[].kind` 가 같은 어휘를 쓴다 —
|
|
# 개인화 행(거리·숨김)이 어느 공용 실체를 가리키는지 이름만 보고 알 수 있어야 한다.
|
|
# ★ STORY 는 여기 없다. 그 다섯(songs·people·chronicle·postcard·quiz)은 kind 가 곧 타입명이고,
|
|
# 코드값 하나(6)를 나눠 쓴다. 아래 표는 kind 가 비어 있던 장소류를 채우기 위한 것이다.
|
|
AREA_KIND = {
|
|
LocalContentType.WEATHER.value: "weather",
|
|
LocalContentType.FESTIVAL.value: "festival",
|
|
LocalContentType.ATTRACTION.value: "attraction",
|
|
LocalContentType.RESTAURANT.value: "restaurant",
|
|
LocalContentType.COURSE.value: "course",
|
|
}
|
|
|
|
# 지역 이야기 여섯. `services/prompts/story.py` 의 산출물 키와 같아야 한다.
|
|
# ★ 순서는 발행본 '지역 이야기' 탭 순서다(`site/sections/items/StorySection.tsx`).
|
|
STORY_KINDS = ("songs", "daily", "people", "chronicle", "postcard", "quiz")
|
|
|
|
|
|
class LocalSource(CodeEnum):
|
|
"""local_contents.source 코드값. 어느 외부 API 에서 왔는지."""
|
|
|
|
OPEN_METEO = 1 # 날씨. API 키 불필요
|
|
TOUR_API = 2 # 한국관광공사. ★ 자체 areaCode 체계 — 카카오 행정구역 코드와 다르다
|
|
KAKAO_LOCAL = 3
|
|
OFFICIAL_WEB = 4 # 지자체·행사 공식 홈페이지에서 운영자가 검수해 등록
|
|
# ★ 지역 이야기 생성분. 출처는 항목 안의 source.url 이고 이 값은 '누가 모았나'다 —
|
|
# 화면이 "AI 가 모았습니다"를 밝힐 근거이자, 나중에 통째로 다시 돌릴 때의 선택자다.
|
|
LLM = 5
|
|
NAVER_CRAWL = 6 # 네이버 플레이스 크롤링(주변 맛집 보강). docs/DECISIONS.md 1-1 예외 — 봇탐지 우회 없이 공개 응답만 읽는다
|
|
|
|
|
|
class LocalContentStatus(CodeEnum):
|
|
"""운영 관리자가 검수하는 지역 콘텐츠 발행 상태."""
|
|
|
|
REVIEW = 1
|
|
PUBLISHED = 2
|
|
ENDED = 3
|
|
|
|
|
|
class TransportType(CodeEnum):
|
|
"""routes.transport 코드값. 가는 길 수단."""
|
|
|
|
CAR = 1
|
|
PUBLIC = 2
|
|
WALK = 3
|
|
|
|
|
|
class SiteStatus(CodeEnum):
|
|
"""sites.status 코드값. ★ 해지는 물리 삭제가 아니라 상태 전이로만 처리한다."""
|
|
|
|
DRAFT = 1
|
|
REVIEW = 2 # 검수 게이트 대기
|
|
PUBLISHED = 3
|
|
SUSPENDED = 4 # 해지 유예 — 페이지는 살아있음
|
|
UNPUBLISHED = 5 # 내림
|
|
|
|
|
|
class BuildStatus(CodeEnum):
|
|
"""site_versions.build_status 코드값. 정적 빌드는 개별 재빌드 단위로 돈다."""
|
|
|
|
PENDING = 1
|
|
BUILDING = 2
|
|
BUILT = 3
|
|
FAILED = 4
|
|
|
|
|
|
class PublishAction(CodeEnum):
|
|
"""publish_logs.action 코드값."""
|
|
|
|
PUBLISH = 1
|
|
UNPUBLISH = 2
|
|
REBUILD = 3
|
|
SUSPEND = 4
|
|
RESUME = 5
|
|
|
|
|
|
class PublishResult(CodeEnum):
|
|
"""publish_logs.result 코드값."""
|
|
|
|
SUCCESS = 1
|
|
REJECTED = 2 # 검수 게이트가 막음 — reject_reason 참고
|
|
FAILED = 3 # 빌드/배포 자체가 실패
|
|
|
|
|
|
class PublishRejectReason(CodeEnum):
|
|
"""publish_logs.reject_reason 코드값. 검수 게이트가 발행을 막은 이유(절대규칙 1~3)."""
|
|
|
|
UNVERIFIED_FACT = 1 # 미검증 fact 포함
|
|
NO_UNIQUE_CONTENT = 2 # 고유 콘텐츠 0건
|
|
JSONLD_MISMATCH = 3 # 구조화 데이터 값 != 화면 값
|
|
REQUIRED_FACT_MISSING = 4 # 업종 스키마 required 필드 누락
|
|
|
|
|
|
class AiEngine(CodeEnum):
|
|
"""ai_check_results.engine 코드값. AI 검색이 우리 사이트를 근거로 답하는지 측정할 대상."""
|
|
|
|
CHATGPT = 1
|
|
PERPLEXITY = 2
|
|
GEMINI = 3
|
|
CLAUDE = 4
|
|
ETC = 99
|
|
|
|
|
|
# ============================================================
|
|
# 작업 큐 (LPS 의 job 큐 구조를 이식 — PostgreSQL 을 큐로 쓴다)
|
|
# ============================================================
|
|
class JobType(CodeEnum):
|
|
"""jobs.job_type 코드값. 수집·비전·빌드는 몇 분씩 걸려 동기 요청으로 처리할 수 없다.
|
|
|
|
무거운 잡(브라우저 필요)과 가벼운 잡(HTTP API 만)을 코드로 갈라 둔다 —
|
|
크롤링 법무 결론이 나면 무거운 잡만 별도 워커 이미지로 분리한다."""
|
|
|
|
COLLECT = 1 # 수집 파이프라인: Perplexity 채널 발견 → 카카오 검증 → 크롤링
|
|
VISION = 2 # 사진 분류 + alt 생성 (Gemini Vision, 20~50장 배치)
|
|
COPY = 3 # 소개문·FAQ 작성 (Gemini 텍스트) — 확보된 fact 만 근거로
|
|
BUILD = 4 # 사이트 정적 빌드 — ★ 개별 재빌드 단위
|
|
LOCAL_SYNC = 5 # 지역 정보 갱신 — 행정구역 코드 단위(같은 지역 사이트 50개여도 1회)
|
|
AI_CHECK = 6 # AI 검색 노출 점검
|
|
SONG = 7 # 이 숙소의 노래 한 곡 (가사 Gemini → 작곡 Suno). 발행이 이 잡을 건다
|
|
|
|
|
|
class JobStatus(CodeEnum):
|
|
"""jobs.status 코드값. 작업 큐 상태.
|
|
|
|
전이는 전부 조건부 원자 UPDATE(CAS)로만 한다. 실패는 재시도 가능하면
|
|
PENDING(run_after=백오프)으로 되돌리고, 소진되면 DEAD(dead-letter)."""
|
|
|
|
PENDING = 1 # 대기(claim 가능). run_after <= now() 일 때만 실제 claim 대상
|
|
RUNNING = 2 # 워커가 점유 중(lease_until 까지 소유). 만료 시 reaper 가 회수
|
|
DONE = 3 # 완료
|
|
DEAD = 4 # dead-letter — max_attempts 소진(수동 개입/알림 대상)
|
|
|
|
|
|
# claim 대상이 되는 활성 상태. dedupe 부분 유니크 인덱스의 조건과 같아야 한다.
|
|
ACTIVE_JOB_STATUSES = {JobStatus.PENDING, JobStatus.RUNNING}
|