최상단을 프로젝트 단위로 평평하게 둔다 — o2o-negosium 과 같은 규약이고, 이 레포만
다르게 갈 이유가 없다. negodata/{backend,front} 가 프로젝트 안에서 f/b 를 가르는 선례,
lps-admin/ 이 백엔드 없이 프론트만 가진 최상단 폴더의 선례다.
backend/ frontend/{admin,site,shared} → solution/{backend,front,site,shared} + admin/
## 왜
내부 라우트(/local-content, /places/:id/seo)의 이름과 화면 코드가 사장님 번들에
그대로 실려 나가고 있었다. UserRole.DEVELOPER 주석의 "고객사에 존재를 노출하지 않는다"를
번들이 깨고 있었다 — 라우트 가드는 화면을 가리지 번들은 못 가린다.
번들을 갈라 확인했다: 사장님 dist 에서 local-content · /places · SeoAudit 이 전부 0건이다.
그 과정에서 두 곳이 더 새고 있었다.
- AppShell 의 NAV 배열이 내부 메뉴를 하드코딩하고 있었다. 앱을 가른 뒤에도 dist 에
local-content 가 남아서 찾았다. 메뉴는 이제 앱이 prop 으로 들고 온다.
- EditorHeader·BuilderPage·LoginPage 가 /places 로 링크하고 있었다. 그 화면이 admin 으로
나갔으니 사장님 앱에서는 404 다. 링크를 걷어내고 LoginPage 기본 도착지는 '/' 로 바꿨다
(앱마다 홈이 다르고 각 라우터의 '/' 가 이미 그걸 안다).
## admin 에 백엔드를 두지 않았다
내부 화면이 부르는 훅이 전부 router/v1/{place,fact,local,validator} 에 이미 있다.
자체 백엔드를 두면 place·fact·link 를 같은 DB 에 대고 두 번 구현하게 된다.
대가는 solution/backend 가 죽으면 admin 도 멈추는 것 — 내부 도구라 감수한다.
## admin 의 `@` 는 solution/front/src 를 가리킨다
내부 화면이 쓰는 API 클라이언트·UI·수집 배선이 solution 에 한 벌만 있고 그 파일들끼리도
`@/...` 로 서로를 부른다. admin 에서 `@` 를 자기 src 로 잡으면 그 참조가 전부 깨진다
(실측 TS2307 14건). 복제하는 길도 있지만 RecollectPanel 주석이 금지한다 —
"수집 경로를 두 벌 만들면 확정 게이트"가 갈라진다.
admin 자기 파일만 `@admin` 이고, 의존 방향은 admin → solution 한 쪽뿐이다.
admin 이 여는 빌더는 다른 오리진이라 절대 URL + 새 탭이다(admin/src/lib/solutionUrl.ts).
react-router Link 로 두면 admin 안에서 라우트를 찾다 404 다.
## 그 밖
- npm 워크스페이스 루트를 레포 루트로 올렸다(admin 이 solution 밖이라).
- docker-compose 를 255→174줄로 줄이고 admin(:3002) 서비스를 넣었다. ADMIN_BIND 기본값은
127.0.0.1 — 0.0.0.0 으로 열면 앱을 가른 의미가 없다.
- 발행 호스트를 프론트 .env 에 따로 적지 않는다. compose 가 루트의 SITE_PUBLIC_HOST 를
VITE_PUBLISH_HOST 로 흘려보낸다 — 두 곳에 적으면 canonical 과 화면 주소가 조용히 갈라진다.
- nginx/site.conf 를 git 에서 빼고 .example 만 남겼다(.env·*.toml 과 같은 규약).
compose 가 bind mount 하므로 클론 직후 복사해야 한다 — 없으면 Docker 가 그 자리에
디렉토리를 만들어 nginx 가 설정 없이 뜬다.
- config.test.toml.example 을 추가했다. 없으면 클론한 사람이 pytest 를 아예 못 돌린다
(conftest import 단계에서 죽는다). 외부 API 키는 전부 빈값이다 —
APP_ENV=test 가 .env 를 안 읽는 이유를 여기서 우회하면 안 된다.
- 경로가 한 칸 깊어져 test_schema_ddl(parents[2]→[3]) 과 test_site_theme 을 고쳤다.
검증: front·admin·site 전부 lint 0 / build 0. 백엔드 514 passed.
남은 4건(test_build_publish 3 · test_snapshot 1)은 이 변경 전부터 실패하던 것으로,
손대지 않은 메인 체크아웃에서 같은 4건이 같게 실패하는 것을 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
177 lines
17 KiB
Markdown
177 lines
17 KiB
Markdown
# DECISIONS
|
|
|
|
미결 사항과, 코드가 그 미결을 어떻게 격리해 두고 있는지를 적는다.
|
|
**결론이 나면 여기에 날짜와 함께 결론을 적고, 해당 플래그/어댑터를 제거한다.**
|
|
|
|
---
|
|
|
|
## 1. 미결 — 결론 전까지 코드로 풀지 않는다
|
|
|
|
### 1-1. 야놀자·여기어때·네이버 플레이스 크롤링의 약관·법적 검토
|
|
|
|
**결론 (2026-08-28) — `docs/DATA_SOURCE_RESEARCH.md` 실측 기준.** 아래 원안은 이력으로 남긴다.
|
|
|
|
| 대상 | 판정 | 근거 |
|
|
|---|---|---|
|
|
| 야놀자 · 여기어때 | **불가** | 풀 브라우저 헤더로도 HTTP 403 + Cloudflare 챌린지. 뚫으려면 봇 탐지 우회가 필요한데 그건 영구 금지 영역이다. 법적으로도 야놀자 v 여기어때 = 형사 무죄(대법원 2022-05-12)지만 **민사 10억 배상 + 복제·저장 금지**(서울중앙지법 2021-08). 우리는 재게시까지 하므로 노출이 더 크다 |
|
|
| 네이버 플레이스 | **robots.txt 기준 불허** | `m.place.naver.com/robots.txt` = `User-agent: * / Disallow: /`. 현재 `naver_place` 어댑터는 위반 상태로 동작 중이다. 사장님 본인 업소 1건·사장님 동의·대량 DB 복제 아님이라 야놀자 사건과는 양상이 다르지만, **이 위에 제품을 세우지 않는다** — 대체 소스가 붙는 대로 발행 payload에서 빼고 대조용 참고값으로 내린다 |
|
|
| 카카오맵 | **불가** | 상세는 SPA 셸(3.6KB), 내부 API 406 차단 |
|
|
| **사장님이 확정한 자체 홈페이지** | **가능** | 사장님 동의 기반. → `StaticHtmlAdapter` 등록(2026-08-28) |
|
|
|
|
부수 결론: **네이버·카카오 생태계는 AI 크롤러를 전면 차단**한다(플레이스·지도·블로그·예약·카카오맵 모두 `Disallow: /`). 즉 그 데이터를 긁어와도 AI 검색에는 원래 없던 정보라 GEO 이득이 없고, 출처를 밝힐 수 없어 `llms.txt`·`AnswerBlock`의 "출처와 검증 시각" 신호도 못 채운다. 공공데이터(TourAPI·LOCALDATA)가 그 자리를 대신한다.
|
|
|
|
| 항목 | 내용 (원안) |
|
|
|---|---|
|
|
| 상태 | ~~미결~~ → **결론남 (2026-08-28)** |
|
|
| 필요한 결론 | 각 사이트의 이용약관·robots.txt 기준으로 자동 수집이 허용되는 범위 |
|
|
| 코드 격리 | `collector` 는 어댑터 패턴. ~~Phase 1 은 `MockAdapter` 만 등록~~ → **2026-08-28: `StaticHtmlAdapter` 등록 완료**(사장님 확정 URL 한정, robots.txt 준수, 플랫폼 호스트는 `_DENY_HOSTS` 로 구조적 차단). `HeadlessAdapter`(Playwright)는 **등록하지 않는다** — Cloudflare 를 뚫는 용도가 되므로 금지 항목과 구분되지 않는다 |
|
|
| 결론이 "불가"일 때 | 폴백 3단계로 간다 — ① 공식 API → ② 사장님이 직접 붙여넣기 → ③ 최소 정보로 생성 + 보완 요청. **생성 자체는 실패시키지 않는다** |
|
|
| 확정 사항 | 캡차 우회 · 봇 탐지 우회 · IP 회전은 **결론과 무관하게 금지**. 구현하지 않는다 |
|
|
|
|
### 1-2. 크롤링한 **이미지**의 재게시 권리
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 상태 | **미결** |
|
|
| 필요한 결론 | OTA/플레이스에 올라간 사진을 우리가 만든 홈페이지에 다시 게시할 수 있는가 (저작권자 = 사장님인가 OTA인가) |
|
|
| 파급 | **막히면 사진 입력원이 "크롤링" → "사장님 업로드"로 바뀌고, Gemini Vision 단계의 입력 자체가 달라진다.** 분류·alt 생성 로직은 같아도 수집 경로가 통째로 교체됨 |
|
|
| 코드 격리 | 사진은 `media` 로 들어오되 **출처(`source_type`)를 반드시 남긴다**(`crawl` / `owner`). 발행 시 `source_type` 로 필터링할 수 있어야 한다 |
|
|
| 대기 중 | 파일 업로드 경로(Azure Blob 클라이언트)는 원본 보일러플레이트에 있으나 **아직 복사하지 않았다.** 이 결론이 난 뒤 media 모듈과 함께 이식한다 |
|
|
| 반영됨 (2026-08-26) | `place.media` 에 `source_type`(owner/crawl)과 `origin_url` 을 NOT NULL / 보존 컬럼으로 두었다. 결론이 "불가"로 나면 발행 시 `source_type = CRAWL` 을 통째로 제외하는 것으로 대응 가능하다 |
|
|
|
|
### 1-3. 관리자에서 수정 허용 범위
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 상태 | **미결** |
|
|
| 필요한 결론 | 사장님이 직접 고칠 수 있는 fact 의 범위. 특히 체크인·취사·반려동물·취소 규정처럼 **틀리면 예약 클레임이 나는 항목**을 자유 입력으로 열 것인가 |
|
|
| 확정 사항 | 수정한 값은 **잠긴다**(`status = CORRECTED`). 자동 갱신이 사장님 수정본을 덮어쓰지 않는다 |
|
|
| 코드 격리 | fact 상태 전이에서 `CORRECTED` 는 자동 수집(`api`/`crawl`/`llm`)이 갱신할 수 없는 종착 상태로 둔다 |
|
|
| 반영됨 (2026-08-26) | `LOCKED_FACT_STATUSES = {CORRECTED}` · `FACT_STATUS_TRANSITIONS[CORRECTED] = {CORRECTED, REJECTED}` — 자동 갱신 경로(UNVERIFIED·VERIFIED)로 돌아갈 수 없다. `tests/test_fact_schema.py` 가 이 규칙을 고정한다 |
|
|
| 남은 결정 | **어떤 필드까지 자유 입력을 허용할 것인가.** 업종 스키마의 `critical: true` 필드가 후보 목록이다 — 숙박 14 / 카페 12 / 음식점 15 / 관광체험 17개 |
|
|
|
|
### 1-4. 해지 시 사이트 처리 정책
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 상태 | **미결** |
|
|
| 필요한 결론 | 해지하면 사이트를 즉시 내리는가 / 유예를 두는가 / 도메인은 어떻게 되는가. AI 검색이 이미 색인한 페이지를 갑자기 404 로 만들면 그 자리를 다시 OTA 가 가져간다 |
|
|
| 코드 격리 | `site` 에 발행 상태를 두고, 해지 처리는 **삭제가 아니라 상태 전이**로만 구현한다. 물리 삭제 경로를 만들지 않는다 |
|
|
| 반영됨 (2026-08-26) | `SiteStatus.SUSPENDED`(해지 유예 — 페이지 살아 있음) / `UNPUBLISHED`(내림)를 분리했다. `PublishAction.SUSPEND`·`RESUME` 으로 `publish_logs` 에 남는다. 유예 기간 길이만 정하면 된다 |
|
|
|
|
---
|
|
|
|
## 2. 이식하면서 내린 결정 (2026-08-26)
|
|
|
|
원본: `o2o-negosium/negodata/backend` 보일러플레이트.
|
|
|
|
| 항목 | 결정 | 이유 |
|
|
|---|---|---|
|
|
| 레이어 구조 | 원본 그대로 — `router` → `service` → `crud`, 람다 DB 실행, `Req_*`/`Res_*` 프로토콜, `RemoveNoneResponse` | "기존 컨벤션을 그대로 따른다" |
|
|
| 포트 | **9800** | negosium 9300 / negodata 9400 / agent 9500 / lps 9600 / anchoring 9700 다음 번호 |
|
|
| DB | `web4ai_db` (테스트 `web4ai_test_db`), 기존 로컬 postgres(`negosium-db` 컨테이너, 5432) 안의 **별도 database** | 원본과 같은 인스턴스·다른 DB. 스키마 네임스페이스 컨벤션 유지 |
|
|
| 마이그레이션 | Alembic 안 씀. `postgres-init/init-data/init.sql` **한 벌**(전체 DDL, 재실행 안전) | 2026-08-31: 누적 ALTER 파일(`alters/`)을 없앴다. 아직 git·서버 어디에도 안 올라가 **보정할 기존 DB 가 없다** — init.sql 에 이미 전부 반영돼 있어 두 벌을 유지할 이유가 없었다. 운영 DB 가 생기는 순간 다시 필요해진다 |
|
|
| 남긴 것 | config 로더 · 로거 · 싱글톤 · DB 세션 매니저(R/W 분리) · gmodel · gtime · authz · JWT/bcrypt dependencies · `company.companies`/`company.users` · auth 라우터 · 스케줄러 껍데기 · conftest(테스트 DB 자동 생성/삭제) | 전 모듈이 공통으로 쓰는 인프라. 인증은 places·facts·sites 전부가 `IsValidAccessToken` 에 의존한다 |
|
|
| 뺀 것 | quotation · supplier · item · card · dashboard · statistics · learning · renegotiation · landing · admin · notification · LPS 연동 · anchoring · 초청메일(ACS/SMTP) · Azure Blob 클라이언트 | negodata 고유 도메인. Blob 클라이언트만 1-2 결론 후 media 모듈과 함께 재이식 예정 |
|
|
| `companies` 테이블 유지 | 유지 | 보일러플레이트의 멀티테넌트 스코프 키(`UserInfo.company_id`)가 전 계층에 박혀 있다. 대행사/운영사 단위로 그대로 쓴다 |
|
|
| ErrorType 구간 | 계정 = 1100. 도메인 구간 예약 — places 1200 / facts 1300 / collector 1400 / generator 1500 / local 1600 / sites 1700 / reports 1800 | 원본이 구간을 나눠 쓰는 방식 유지 |
|
|
| 외부 API 키 | `[ExternalApiConfig]` 로 toml + env override. **키가 비면 해당 어댑터만 비활성, 서버는 그대로 뜬다** | 부팅이 외부 계약에 묶이면 안 됨 |
|
|
| 백그라운드 작업 | 원본에 전용 작업 큐 없음(APScheduler 크론만). 수집·비전분석·빌드는 몇 분 걸리므로 **큐를 새로 얹어야 한다** — 방식 미정 | 원본에 없는 것이라 팀 컨벤션 확인 필요. 아래 3번 참고 |
|
|
|
|
---
|
|
|
|
## 3. 다음 단계에서 정해야 할 것 (법무 이슈 아님)
|
|
|
|
- **작업 큐 구현 방식.** 원본 보일러플레이트엔 APScheduler 크론만 있고 작업 큐가 없다.
|
|
수집 + 비전 분석 + 빌드는 동기 요청으로 처리할 수 없다(몇 분). 후보: DB 테이블 기반 잡 큐(추가 인프라 0) / Celery + Redis / arq.
|
|
→ **DB 잡 테이블 + 상태 폴링 API** 를 기본안으로 제안. 완료 시 알림톡.
|
|
- **TourAPI areaCode ↔ 카카오 행정구역 코드 매핑 테이블.** 두 체계가 다르므로 매핑을 데이터로 관리할지 코드로 박을지.
|
|
- **지역 정보 캐시 TTL.** 행정구역 코드 단위 캐싱은 확정. 날씨/축제/관광지 각각의 갱신 주기만 정하면 된다.
|
|
|
|
---
|
|
|
|
## 4. Phase 2 에서 내린 결정 (2026-08-26)
|
|
|
|
작업 순서 2번 — 4개 업종 스키마 정의 + 마이그레이션.
|
|
|
|
| 항목 | 결정 | 이유 |
|
|
|---|---|---|
|
|
| 업종 스키마 위치 | `common/category_schema/resources/*.json` + 로더 | 원본의 `common/anchoring/resources/` 패턴 그대로. **업종 추가 = 파일 1개 + enum 1줄** |
|
|
| fact 저장 | key-value (`facts.key` / `value` / `unit`) | 업종마다 필드가 완전히 달라 컬럼으로 못 편다 |
|
|
| 활성 fact 유니크 | `(place_id, unit_id, key)` 당 1건. **`REJECTED`·`EXPIRED` 는 제외** | 같은 항목에 두 값이 동시 노출되는 것을 DB 가 막는다. 틀린 값·만료 값은 이력으로 남겨야 하므로 유니크에서 뺀다 |
|
|
| 유니크 인덱스 2분할 | `unit_id IS NULL` / `IS NOT NULL` 로 나눠 건다 | Postgres 에서 NULL 끼리는 유니크가 안 걸린다. 나누지 않으면 사업장 단위 fact 가 중복된다 |
|
|
| `critical` 플래그 | 업종 스키마 필드 속성으로 도입 | 절대규칙 1(미검증 fact 노출 금지)의 대상 목록이 코드가 아니라 데이터에 있어야 업종 추가 시 자동으로 따라온다 |
|
|
| `allow_llm` 플래그 | 기본 `False`. `True` 는 소개문 계열 2개뿐 | 절대규칙 7(LLM 은 사실을 만들지 않는다)을 스키마 레벨에서 강제. 테스트가 `required` 필드의 `allow_llm=True` 를 금지한다 |
|
|
| 지역 정보 캐시 키 | `local_contents.region_code` (place_id 아님) | 같은 지역에 사이트 50개가 생겨도 외부 조회는 1회. 카카오 키워드 검색이 좌표 변환보다 4배 비싸다 |
|
|
| 스키마 네임스페이스 | `place` / `fact` / `local` / `site` | 원본의 도메인별 schema 컨벤션. `local` 은 Postgres 비예약어라 그대로 쓸 수 있다(확인함) |
|
|
| 테이블명 | 복수형 (`places`, `facts`) | 원본이 복수형(`companies`, `users`, `quotations`). 스펙 문서의 단수 표기는 엔티티 이름으로 읽었다 |
|
|
| `server_default` | 신규 도메인 테이블에만 추가 | ORM `default=` 는 Python 쪽이라 raw INSERT 에 안 먹는다. `create_all`(테스트 DB)과 `init.sql`(실 DB)이 갈라져서 실제로 버그가 났다. **`companies`/`users` 는 원본 그대로 두었다** |
|
|
| ORM ↔ init.sql 정합성 | `tests/test_schema_ddl.py` 가 파일을 파싱해 대조 | 스키마 정의가 두 곳에 있는 구조(원본 컨벤션)라, 드리프트를 테스트로 막는다 |
|
|
| 외부 API 키 주입 | 레포 최상위 `.env` + `python-dotenv` | 우선순위 = 실제 환경변수 > `.env` > toml. **`APP_ENV=test` 면 `.env` 를 읽지 않는다** — 실키가 새면 테스트가 외부 API 를 때리고 요금이 나간다 |
|
|
| 키 출처 | Perplexity·Gemini 는 `o2o-infinith-backend/.env` 값 재사용. 카카오·TourAPI 는 **미발급** | 사내 어디에도 카카오/TourAPI 키가 없다. 발급 후 `.env` 에 채워야 6번 단계가 돈다 |
|
|
|
|
### 아직 테이블이 없는 것
|
|
|
|
- **`report` 스키마** — 노출 리포트·유입 통계(GA4 Data API, Search Console API). 작업 순서 6번 이후.
|
|
- **작업 큐** — 아래 3번의 미결 사항. 수집·비전분석·빌드를 담을 잡 테이블이 아직 없다.
|
|
- **TourAPI areaCode ↔ 카카오 행정구역 코드 매핑** — 테이블 대신 `common/category_schema` 와 같은 리소스 JSON 으로 두는 것을 제안. 3번 참고.
|
|
|
|
---
|
|
|
|
## 5. Phase 3 에서 내린 결정 (2026-08-27)
|
|
|
|
작업 순서 3번 — places · facts API(검증 상태 전이) + 작업 큐.
|
|
|
|
### 5-1. fact 를 '노출값 1건 + 후보 N건' 으로 바꿨다
|
|
|
|
처음엔 활성 유니크를 `status IN (1,2,3,4)` 로 걸었다. 재수집(업데이트)을 넣어 보니 세 가지가 깨졌다 —
|
|
실제로 돌려서 확인한 결과다:
|
|
|
|
| 상황 | 깨진 것 |
|
|
|---|---|
|
|
| 값이 그대로인데 재수집 | 검증(VERIFIED)이 초기화돼 사이트에서 사실이 사라졌다 |
|
|
| 값이 바뀐 재수집 | 확인된 노출값이 즉시 밀려나 사이트가 비었다 |
|
|
| 정정본(CORRECTED)에 재수집 | `FACT_LOCKED` 로 크롤링 값을 통째로 버려 OTA 불일치 신호가 사라졌다 |
|
|
|
|
원인은 하나 — **유니크가 "노출값"과 "새로 들어온 값"을 동시에 못 갖게 막고 있었다.**
|
|
|
|
**결정: 유니크를 노출 상태(VERIFIED·CORRECTED)에만 건다.**
|
|
|
|
```
|
|
노출값 VERIFIED / CORRECTED (place, unit, key) 당 1건 ← 사이트에 나감
|
|
후보 UNVERIFIED / PENDING_OWNER 여러 건 공존 ← 재수집이 쌓임
|
|
이력 REJECTED / EXPIRED
|
|
```
|
|
|
|
이걸로 세 프로세스가 분리됐다.
|
|
|
|
| 프로세스 | 동작 | outcome |
|
|
|---|---|---|
|
|
| 생성 | 자동 수집 → 후보 → 사람 승인 → 노출 | `CANDIDATE_CREATED` |
|
|
| 업데이트(값 같음) | 검증 유지, 확인 시각만 갱신 | `REFRESHED` |
|
|
| 업데이트(값 다름) | **노출값 유지** + 후보 적재 | `CANDIDATE_CREATED` |
|
|
| 승인 | 후보 → 노출값, 옛 값 EXPIRED | `PUBLISHED_REPLACED` |
|
|
| 수정(사람 직접) | 즉시 노출값 교체 | `PUBLISHED_REPLACED` |
|
|
|
|
스키마: `postgres-init/init-data/init.sql` (`fact.facts` 활성 유니크 + 후보 상태)
|
|
|
|
### 5-2. 그 밖의 결정
|
|
|
|
| 항목 | 결정 | 이유 |
|
|
|---|---|---|
|
|
| 사람 직접 입력 | 즉시 노출값(VERIFIED)으로 들어간다 | 넣은 사람이 곧 출처이자 책임 주체다. 입력→확인 2단계로 만들면 실무에서 안 쓴다. 검수 게이트의 실제 목적은 **자동 수집**을 거르는 것 |
|
|
| 정정본 잠금의 의미 | "덮어쓰기 금지"이지 "기록 금지"가 아니다 | 크롤링 값을 버리면 OTA 가 아직 다른 값이라는 신호를 잃는다. 후보로 남겨 사람이 본다 |
|
|
| `places.content_updated_at` | 노출값이 **실제로 바뀔 때만** 찍는다 | 개별 재빌드 대상 판별(`site_versions.built_at < content_updated_at`). 후보만 쌓였을 땐 안 찍힌다 — 사이트 내용이 안 바뀌었으니 재빌드가 불필요 |
|
|
| 작업 큐 | LPS(`o2o-negosium/lps`)의 `job` 큐를 그대로 이식 | 사내 선례이고 도커에서 검증된 구조. 원자적 claim(`FOR UPDATE SKIP LOCKED`) + lease 소유권 + dedupe + dead-letter. Redis/Celery 를 안 쓰는 이유는 운영 컴포넌트가 늘면 장애 지점이 늘기 때문 |
|
|
| 큐 세션 진입점 | `DB_SESSION_MNG.execute_lambda_write` 를 새로 추가 | 큐 전이는 `UPDATE ... RETURNING` 이라 조회/변경을 못 나눈다. 기존 `execute_lambda_run`(ErrorType만) / `execute_lambda_claim`(rowcount만) 로는 값을 못 받는다 |
|
|
| `jobs.job_id` server_default | 이 테이블만 PK 에 `gen_random_uuid()` 를 건다 | 큐만 raw SQL 로 INSERT 해서 ORM 의 Python default 가 안 먹는다. `create_all`(테스트 DB)과 `init.sql`(실 DB)이 갈라져 실제로 버그가 났다 |
|
|
| 수집 중복 방지 | `dedupe_key = "collect:{place_id}"` | 사업장당 활성 수집 잡 1건. 버튼을 두 번 눌러도 두 번 돌지 않는다 |
|
|
|
|
### 5-3. 아직 안 한 것
|
|
|
|
- **`검색어 → URL 발견 → 크롤링 → 에셋` 체인은 아직 안 돈다.** 작업순서 4번(collector).
|
|
지금은 URL 을 **받아서** 저장하는 그릇만 있다(Perplexity 자동 호출 없음).
|
|
- 사진(`place.media`)은 테이블만 있고 업로드·저장 경로가 없다. 이미지 재게시 권리(1-2)가 미결이라
|
|
Azure Blob 클라이언트를 일부러 아직 이식하지 않았다.
|
|
- 카카오 REST API 키 / TourAPI 키가 사내 어디에도 없다. 발급해서 `.env` 에 채워야 실제 연동이 돈다.
|