This commit is contained in:
김성경 2026-09-18 13:01:41 +09:00
commit 176a77a231
2 changed files with 23 additions and 3 deletions

View File

@ -51,6 +51,13 @@ services:
<<: *common-env
# ★ 크론은 이 컨테이너에서만 돈다. 프로세스가 여럿이면 같은 시각에 중복 실행된다.
SCHEDULER_ENABLED: "1"
# ★ GSC_CREDENTIALS_HOST_FILE(호스트 경로, .env)을 아래 볼륨으로 이 컨테이너 안에 마운트한
# 고정 자리다. search_console_settings.load_settings() 가 실제로 읽는 건 이 값이다 —
# 호스트 경로를 코드에 그대로 넘기면 컨테이너 안에서 그 경로가 없어 실패한다.
# 실측(2026-09-18): 이 줄이 없어서 GSC_ENABLED=1 인데도 10분마다
# ValueError(GSC_CONFIG_MISSING) 로 조용히 실패하고 있었다 — 로그엔 BATCH_FAILED 만 남아
# 원인이 안 보였다.
GSC_CREDENTIALS_FILE: /app/secrets/gsc-credentials.json
volumes:
- ./solution/site/payloads:/app/solution/site/payloads
# 노래 파일. payload 와 나란히 둔다 — 백엔드가 mp3 를 여기 떨구면 워커가 렌더러를
@ -59,6 +66,9 @@ services:
# ★ 스키마 마이그레이션 SQL. 이미지에 굽지 않고 마운트한다 — 파일이 자주 늘고,
# 이미 세운 DB 를 따라오게 하는 것이 목적이라 코드 배포와 별개로 돌 수 있어야 한다.
- ./postgres-init:/app/postgres-init:ro
# ★ GSC_CREDENTIALS_HOST_FILE 이 비어 있으면 /dev/null 을 마운트한다 — 빈 문자열을 그대로
# 쓰면 컴포즈 볼륨 문법이 깨진다. GSC_ENABLED=0 이면 이 파일은 아예 안 읽으므로 무해하다.
- ${GSC_CREDENTIALS_HOST_FILE:-/dev/null}:/app/secrets/gsc-credentials.json:ro
ports:
- "${API_BIND:-0.0.0.0}:${API_PORT:-9800}:9800"
extra_hosts:

View File

@ -1056,21 +1056,31 @@ function writePreviewShell(outRoot: string, assets: {script: string; css: string
/**
* 이미 구워져 있는 사이트에서 오리진을 알아낸다. 한 장도 없으면 빈 문자열.
*
* ★ payload 를 읽지 않는 경로(`--refresh-assets` 기동)에서 루트 색인 파일을 다시 쓰려면
* ★ payload 를 읽지 않는 경로(`--seed-assets` 기동)에서 루트 색인 파일을 다시 쓰려면
* 오리진이 필요한데, 그 값은 payload 에만 있다. 디스크에 있는 페이지가 자기 canonical 로
* 선언해 둔 것을 쓴다(readBakedOrigin 주석).
* ★ **가장 최근에 구워진 페이지**의 오리진을 쓴다 — 처음 만난 것을 쓰면(readdirSync 순서는
* 보장이 없다) 호스트를 옮긴 뒤 한 번도 재발행 안 한 옛 사이트(목업 포함) 하나가 걸릴 때
* 전체 sitemap.xml 이 죽은 옛 호스트로 통째로 구워진다. 실측(2026-09-18): SITE_PUBLIC_HOST
* 를 web4ai.o2osolution.ai 로 옮긴 지 오래인데, solution-worker 가 재시작(배포)될 때마다
* 이 함수가 옛 호스트(w4ai.o2o.kr)로 구운 사이트 하나를 집어 사이트맵 전체가 죽은 주소로
* 덮였다 — 오늘 새로 발행한 사이트까지 사이트맵에서 옛 호스트로 나갔다. 갓 구운 페이지일수록
* 지금 SITE_PUBLIC_HOST 를 반영했을 확률이 높다.
*/
function findBakedOrigin(outRoot: string): string {
const sitesDir = join(outRoot, SITE_DIR);
if (!existsSync(sitesDir)) return '';
let newest: {origin: string; mtimeMs: number} | null = null;
for (const entry of readdirSync(sitesDir, {withFileTypes: true})) {
if (!entry.isDirectory()) continue;
const file = join(sitesDir, entry.name, 'index.html');
if (!existsSync(file)) continue;
const origin = readBakedOrigin(readFileSync(file, 'utf-8'));
if (origin) return origin;
if (!origin) continue;
const mtimeMs = statSync(file).mtimeMs;
if (!newest || mtimeMs > newest.mtimeMs) newest = {origin, mtimeMs};
}
return '';
return newest?.origin ?? '';
}
function writeRootMachineFiles(outRoot: string, origin: string) {