런타임은 한 줄도 안 바뀌었다. 채널을 모르게 만들어 둔 것이 여기서 값을 했다 — 새로 생긴 것은 형식 변환(kakao_bot)과 대화 상태(channel)뿐이다. ★★ 오픈빌더는 서명을 주지 않는다. URL 만 알면 누구나 때릴 수 있고 userRequest.user.id 를 위조하면 그 사장님 행세를 한다 — 1단계의 신원 연결이 통째로 무의미해지는 자리다. 공유 시크릿(헤더 X-Agent-Secret, compare_digest) + 선택적 KAKAO_BOT_ID 대조로 막고, 시크릿이 없으면 엔드포인트가 404 다 (401 은 "여기 뭔가 있다" 를 알려 준다). - router/v1/agent/kakao_bot: 카카오 형식을 아는 유일한 파일. 헤더·경로 두 경로 - services/agent/channel: 신원(★ 토큰을 발급하지 않는다) · 가게 고르기 · 확인 - 0022: owner_kakao_links 에 current_place_id · pending_* 빌더 화면과 다른 것 셋: - 로그인 토큰이 없다 → 발화자 키로 사장님을 찾는다 - place_id 가 URL 에 없다 → 여럿이면 추측하지 않고 되묻는다. 임의로 첫 가게를 고르면 사장님은 엉뚱한 가게를 고쳐 놓고도 모른다 - 확인을 되돌려 줄 프론트가 없다 → 서버가 pending 을 든다. ★ 3분 만료가 없으면 한참 뒤의 "네" 한 마디에 묵은 발행이 돈다 5초 벽은 DEADLINE_SEC=4.0 으로 끊고, 어떤 실패도 200+안내다 — 메신저에서는 500 도 침묵으로 보인다. 밟은 것: execute_lambda 는 람다 반환값을 그대로 준다(CRUD 관례가 (ErrorType,값)). 우리 람다가 객체만 돌려주자 언패킹 TypeError 가 났고, 라우터가 예외를 삼켜 화면에는 안내 한 줄만 보였다 — 원인이 안 보이는 종류다. test_kakao_webhook.py 17 passed. 전체 841 passed / 53 failed(이전과 동일). npm run lint 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
204 lines
11 KiB
Plaintext
204 lines
11 KiB
Plaintext
# cp .env.example .env 후 값을 채운다. .env 는 커밋되지 않는다.
|
|
# 우선순위: 실제 환경변수(compose) > .env > 코드 기본값(config_models.py)
|
|
#
|
|
# ★ 값 뒤에 주석을 붙이지 않는다. compose 의 `env_file` 은 줄 끝 주석을 **값으로 읽는다** —
|
|
# `AZURE_STORAGE_CONNECTION_STRING= # 비우면...` 은 "빈 값"이 아니라 "# 비우면..." 이라는 값이다.
|
|
# 실측(2026-09-07): 그래서 Azure 를 끈 로컬에서 발행 잡이 업로드를 시도하고
|
|
# "Connection string is either blank or malformed" 로 죽었다. 게이트는 통과했는데 발행만 실패한다.
|
|
# 주석은 반드시 **윗줄**에 둔다.
|
|
|
|
# ── 공통 solution/backend · admin/backend (server_configs 가 읽는다)
|
|
APP_ENV=local
|
|
|
|
# ★ compose 로 띄우면 `host.docker.internal` 이다 — 컨테이너 안의 127.0.0.1 은 그 컨테이너다.
|
|
# 127.0.0.1 은 백엔드를 **네이티브로**(.venv/bin/python) 돌릴 때만 맞다.
|
|
# 이 값을 그대로 두고 `docker compose up` 하면 API 는 healthz 200 으로 멀쩡해 보이는데
|
|
# 워커만 조용히 재시작을 반복한다(ConnectionRefusedError 5432) — 발행 잡이 영원히 안 돈다.
|
|
DB_HOST=host.docker.internal
|
|
DB_PORT=5432
|
|
DB_USER=postgres
|
|
DB_PASSWORD=
|
|
DB_NAME=web4ai_db
|
|
|
|
# 비우면 토큰 서명이 안 된다
|
|
# 생성: python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|
JWT_ACCESS_SECRET=
|
|
JWT_REFRESH_SECRET=
|
|
|
|
# 키가 비면 그 어댑터만 꺼진다. 서버는 뜬다.
|
|
PERPLEXITY_API_KEY=
|
|
# Perplexity 채널 발견. 0=끔(기본)
|
|
COLLECT_USE_PERPLEXITY=0
|
|
NAVER_CLIENT_ID=
|
|
NAVER_CLIENT_SECRET=
|
|
# 미발급. 없으면 네이버 지역검색을 쓴다
|
|
KAKAO_REST_API_KEY=
|
|
OPENAI_API_KEY=
|
|
# 디코딩된 키(인코딩 키는 이중 인코딩된다)
|
|
TOUR_API_KEY=
|
|
# 발행할 때 이 숙소의 노래를 한 곡 만든다(가사 Gemini → 작곡 Suno).
|
|
# 비우면 그 단계만 건너뛴다 — 발행은 그대로 된다.
|
|
# ★ 콜백은 쓰지 않고 폴링한다(우리 서버는 Suno 가 닿을 수 있는 주소가 아니다).
|
|
# 그래도 API 가 필수로 요구하는 필드라 값을 채워 보낸다.
|
|
SUNO_API_KEY=
|
|
SUNO_CALLBACK_URL=https://example.com/api/suno/callback
|
|
# 발행 사이트 메타 키워드(keywords · 제목)를 받아 올 SiteOntology 주소(o2o-site-ontology, 기본 :3100).
|
|
# 비우면 그 단계만 건너뛴다 — 제목·메타가 예전 그대로 나간다.
|
|
# ★ 워커가 부르는 주소다. compose 로 띄우면 컨테이너 안에서 보는 주소(http://host.docker.internal:3100),
|
|
# 백엔드를 네이티브로 돌리면 http://127.0.0.1:3100
|
|
SITE_ONTOLOGY_URL=
|
|
|
|
# 프리렌더가 절대 굽지 않는 슬러그(쉼표 구분). 손으로 만든 목업(/s/stay·stay2·stay3·stay4·stay5)
|
|
# 이름과 같은 슬러그로 실제 발행이 생기면 그 payload 로 목업을 덮어 구워버린다 — 비우지 않는다.
|
|
PRERENDER_PROTECTED_SLUGS=stay,stay2,stay3,stay4,stay5
|
|
|
|
# ── SNS 게재(스레드) ────────────────────────────────────────────────
|
|
# 사장님이 [SNS에 알리기] 를 누르면 확인된 fact 로 짧은 글을 쓰고, 승인을 받아
|
|
# **사장님 개인 계정**으로 올린다. 비우면 그 기능만 꺼진다(서버는 뜬다).
|
|
#
|
|
# ★ SOCIAL_TOKEN_SECRET 이 없으면 계정 연결 자체를 막는다 — 위임받은 토큰을
|
|
# 평문으로 보관하는 길을 열지 않는다. 우리 API 키와 성격이 다르다:
|
|
# API 키는 우리 돈이 나가고, 이 토큰은 **사장님 이름으로 글이 나간다.**
|
|
SOCIAL_TOKEN_SECRET=
|
|
# ★ 실제 게시는 이 값이 '1' 일 때만 열린다. 플랫폼 계약과 해지 안내 페이지 정책
|
|
# (DECISIONS 1-4)을 확인하기 전에는 초안·승인까지만 돌린다 — 게시는 되돌릴 수 없다.
|
|
SOCIAL_POSTING_ENABLED=0
|
|
# 승인 요청의 수명. 지나면 EXPIRED 로 내려가고 화면에 '만료됨 · 다시 보내기' 로 남는다.
|
|
SOCIAL_APPROVAL_HOURS=24
|
|
# 승인 화면이 열리는 주소(빌더 SPA). 알림톡 버튼이 이 주소로 간다.
|
|
SOCIAL_APP_ORIGIN=
|
|
THREADS_APP_ID=
|
|
THREADS_APP_SECRET=
|
|
THREADS_REDIRECT_URI=
|
|
# 알림톡(대행사). 비면 발송을 건너뛰고 빌더 화면 승인만 쓴다 — 기능은 그대로 돈다.
|
|
# ★ 템플릿 코드는 심사 대상이라 env 로 둔다. 반려로 코드가 바뀌면 배포 없이 고쳐야 한다.
|
|
ALIMTALK_API_KEY=
|
|
ALIMTALK_API_SECRET=
|
|
ALIMTALK_PROFILE_ID=
|
|
ALIMTALK_SENDER=
|
|
ALIMTALK_TEMPLATE_CODE=
|
|
|
|
# ── 사장님 에이전트 · 카카오톡 채널 연결 ──────────────────────────────
|
|
# 사장님이 카톡으로 사이트를 고치려면, 채널 발화자(채널 단위 익명 키)를 우리 계정에
|
|
# 묶어야 한다. 빌더에서 코드를 받아 채널에 한 번 입력하는 절차다.
|
|
# ★ 이 값이 비면 연결 화면이 아예 안 뜬다 — 어디에 코드를 칠지 말해 줄 수 없는데
|
|
# 코드만 발급하면 사장님에게는 고장난 화면이다.
|
|
# 사장님 대화창(에이전트). 1=사용, 0=감춤.
|
|
# ★ 1 이어도 LLM 키가 없으면 안 열린다 — 키 없는 환경에서 켜 둔 채 잊어도
|
|
# "눌러도 안 되는 입구" 가 생기지 않는다.
|
|
AGENT_CHAT_ENABLED=1
|
|
# 카카오톡 채널 웹훅(오픈빌더 스킬 서버). ★ 오픈빌더는 서명을 주지 않는다 —
|
|
# URL 만 알면 누구나 때릴 수 있고 발화자 id 를 위조하면 그 사장님 행세를 한다.
|
|
# 비우면 웹훅 엔드포인트가 404 다(반쯤 열린 상태를 만들지 않는다).
|
|
# 만드는 법: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
KAKAO_WEBHOOK_SECRET=
|
|
# 우리 봇이 맞는지 한 겹 더. 오발송을 거르는 용도라 비워도 된다.
|
|
KAKAO_BOT_ID=
|
|
KAKAO_CHANNEL_PUBLIC_ID=
|
|
KAKAO_LINK_CODE_TTL_MIN=10
|
|
KAKAO_LINK_MAX_ATTEMPTS=5
|
|
|
|
|
|
# 구글 로그인. 비우면 구글 로그인만 꺼진다(서버는 뜨고, 화면에 버튼도 안 뜬다).
|
|
# Google Cloud Console > API 및 서비스 > 사용자 인증 정보 > OAuth 2.0 클라이언트 ID(웹 애플리케이션)
|
|
# "승인된 JavaScript 원본" 에 화면 주소를 등록해야 브라우저에서 토큰이 나온다(리디렉션 URI 는 필요 없다).
|
|
# ★ 백엔드(aud 대조)와 프론트(버튼)가 **같은 값**을 써야 한다 — compose 가 이 하나를
|
|
# VITE_GOOGLE_CLIENT_ID 로 흘려보낸다. 두 곳에 따로 적지 않는다.
|
|
# ★ 바꾸면 프론트를 다시 구워야 한다: ./deploy.sh solution-site
|
|
GOOGLE_CLIENT_ID=
|
|
|
|
# CORS 허용 오리진. 쉼표로 여럿.
|
|
# 서버에 올리면 반드시 적는다. 안 적으면 화면은 뜨고 API 만 막힌다.
|
|
# CLIENT_URL=http://172.30.1.36:30031,http://localhost:3002
|
|
# LANDING_URL=
|
|
|
|
# ── solution/frontend 브라우저가 부르는 주소 (compose 가 VITE_* 로 주입)
|
|
# ★ 브라우저가 부르는 주소다. 서버에 올리면 localhost 는 즉시 틀린다.
|
|
# ★ **앱과 같은 오리진을 적는다.** nginx(:80)가 /v1 을 같은 오리진으로 프록시하므로
|
|
# (nginx/site.conf) 앱이 부를 주소는 `:9800` 이 아니라 앱 주소 그 자체다. `:9800` 을 적으면
|
|
# 스스로 크로스 오리진을 만들어 CORS 가 붙고, 화면은 뜨는데 **로그인만 계속 실패한다** —
|
|
# 서버는 200 에 토큰까지 내려보내고 브라우저가 allow-origin 이 없어 그 응답을 버린다.
|
|
# 실측(2026-09-07): 이 기본값 그대로 띄우면 :80 으로 연 앱에서 로그인이 안 된다.
|
|
# ★ 값을 바꾸면 번들을 다시 구워야 한다: ./deploy.sh solution-site
|
|
PUBLIC_API_BASE_URL=http://localhost
|
|
PUBLIC_WEB_BASE_URL=http://localhost
|
|
|
|
# ── solution/site 발행물 — solution/backend 도 같이 본다
|
|
# canonical·og:url·sitemap·IndexNow 가 전부 SITE_PUBLIC_HOST 를 쓴다.
|
|
# 로컬은 비워 둔다(기본값 localhost). 서버에 올릴 때만 실제 도메인을 적는다.
|
|
# SITE_PUBLIC_HOST=web4ai.o2osolution.ai
|
|
# 비우면 색인 통보를 건너뛴다(발행은 정상)
|
|
INDEXNOW_KEY=
|
|
|
|
# Google Search Console — 최초 소유권/서비스 계정 권한 설정 후 켠다 (docs/SEARCH_CONSOLE.md).
|
|
GSC_ENABLED=0
|
|
GSC_PROPERTY_URL=
|
|
GSC_CREDENTIALS_FILE=
|
|
GSC_CREDENTIALS_HOST_FILE=
|
|
GSC_ALERT_DAYS=7
|
|
GSC_ALERT_WEBHOOK_URL=
|
|
|
|
# 장애 알림(잡 dead-letter·발행 업무 실패·부분 실패·잡 큐 정체) — Teams Workflows 수신 webhook.
|
|
# GSC_ALERT_WEBHOOK_URL 과 다른 값이다(그건 색인 감시 전용) — docs/ALERTS.md.
|
|
# 비우면 알림은 DB(alert_outbox)에 쌓이기만 하고 안 나간다. 서버 동작에는 영향 없다.
|
|
TEAMS_WEBHOOK_URL=
|
|
# 재시도마다 중복 스팸을 막는 창(분). 기본 60분 — 같은 사유가 이 시간 안에 또 터지면 다시 안 보낸다.
|
|
ALERT_DEDUPE_WINDOW_MIN=60
|
|
|
|
# 비우면 로컬 발행만 한다
|
|
AZURE_STORAGE_CONNECTION_STRING=
|
|
AZURE_STORAGE_CONTAINER=
|
|
AZURE_STORAGE_PREFIX=
|
|
|
|
# ── compose 포트 매핑
|
|
# 비우면 로컬 기본값. 서버 값은 docs/SERVERS.md
|
|
# SITE_HTTP_PORT=80 # 발행 사이트
|
|
# WEB_PORT=3000 # 사장님 앱
|
|
# API_PORT=9800 # 사장님 API
|
|
# ADMIN_PORT=3002 # 내부 화면 (bind 127.0.0.1)
|
|
# ADMIN_API_PORT_PUBLIC=9801 # 내부 API (bind 127.0.0.1)
|
|
|
|
# 자동 로그인 — 위저드 앞에 로그인 화면을 세우지 않으려고 세션을 미리 잡는다.
|
|
# ⚠️ 이 값은 **프론트 번들에 구워진다.** 페이지를 연 사람은 누구나 JS 에서 읽는다 —
|
|
# 내부 테스트 호스트에서만 채우고, 사장님에게 여는 순간 비운다(lib/autoSession.ts).
|
|
# ★ solution-frontend(--profile dev, vite dev)에서만 읽힌다 — 운영 진입점(solution-site,
|
|
# nginx/Dockerfile)은 이 값을 build arg 로 아예 받지 않는다. 여기 채워도 운영 번들에는
|
|
# 절대 안 들어간다. 바꾸면 재기동만 하면 된다(운영 이미지 재빌드가 필요 없다).
|
|
AUTO_LOGIN_ID=
|
|
AUTO_LOGIN_PW=
|
|
|
|
# ── 메일 발송 (예약 요청 알림) ────────────────────────────────────────────
|
|
# 비우면 메일을 보내지 않는다. 서버는 그대로 뜨고, 예약 요청 폼은 "전화로 문의" 로 답한다.
|
|
# ★ 1순위는 회사 공용 Azure Communication Services 다(negodata 와 같은 리소스).
|
|
# 발신 도메인의 SPF·DKIM 을 그쪽이 관리하므로 메일서버를 새로 세울 필요가 없다.
|
|
ACS_EMAIL_ENDPOINT=
|
|
ACS_EMAIL_ACCESSKEY=
|
|
ACS_EMAIL_SENDER=
|
|
|
|
# ACS 를 못 쓰는 환경의 폴백. 이쪽을 쓰면 SPF·DKIM 을 직접 걸어야 한다.
|
|
SMTP_HOST=
|
|
SMTP_PORT=587
|
|
SMTP_USER=
|
|
SMTP_PASSWORD=
|
|
SMTP_FROM=
|
|
SMTP_FROM_NAME=Web4AI
|
|
# starttls(587) · ssl(465) · plain. 비우면 포트로 고른다.
|
|
SMTP_TLS=
|
|
|
|
# SNS — Threads 우선(2026-09-14). SOCIAL_TOKEN_SECRET은 Fernet.generate_key() 형식의 키.
|
|
# 키·앱 설정 없으면 연결 비활성, 초안/복사/화면 확인은 동작한다.
|
|
SOCIAL_TOKEN_SECRET=
|
|
THREADS_APP_ID=
|
|
THREADS_APP_SECRET=
|
|
THREADS_REDIRECT_URI=https://web4ai.o2osolution.ai/v1/social/oauth/callback
|
|
SOCIAL_APP_ORIGIN=https://web4ai.o2osolution.ai
|
|
SOCIAL_APPROVAL_HOURS=24
|
|
# 앱 심사·테스트 계정 게시·해지 안내 페이지 정책 검증 후 활성화.
|
|
SOCIAL_POSTING_ENABLED=0
|
|
# 대행사 선택 전 비워 둔다. 현재 어댑터는 SOLAPI 계약이며 교체는 external/alimtalk.py만.
|
|
ALIMTALK_API_KEY=
|
|
ALIMTALK_API_SECRET=
|
|
ALIMTALK_PROFILE_ID=
|
|
ALIMTALK_SENDER=
|
|
ALIMTALK_TEMPLATE_CODE=
|