Merge branch 'main' of https://gitea.o2o.kr/Web4ai/o2o-site-AEO
This commit is contained in:
commit
ab158be16f
37
postgres-init/migrations/0000_drop_companies.sql
Normal file
37
postgres-init/migrations/0000_drop_companies.sql
Normal file
@ -0,0 +1,37 @@
|
||||
-- 0000 · 회사(테넌트) 제거를 기존 DB 에 적용한다 — owner_user_id 백필 → company_id 삭제 → companies 삭제
|
||||
--
|
||||
-- ★ 왜 이 파일이 늦게 생겼나 (2026-09-11)
|
||||
-- 회사 제거(94551af, 2026-09-08)는 이 폴더가 생기기(09-09) **전**의 변경이라 init.sql 안의
|
||||
-- DO 블록으로만 있었다. 09-10 init.sql 을 현재 모습으로 다시 쓰면서 그 블록이 사라졌고,
|
||||
-- 로컬 DB 는 그 사이 init.sql 로 이미 따라와 있어서 아무도 몰랐다.
|
||||
-- 실측(킹서버): 회사가 살아 있는 채로 남은 DB 는 0005 의 `DROP SCHEMA company RESTRICT` 에서
|
||||
-- 멈춘다(company.companies 3행) — 0001~0010 이 하나도 못 들어간다.
|
||||
--
|
||||
-- ★ 왜 0000 인가 — 0005 보다 먼저 돌아야 하고, 시간 순으로도 폴더의 어떤 변경보다 앞이다.
|
||||
-- 새 파일은 여전히 번호를 이어 붙인다(README). 이 번호는 이 파일 하나의 예외다.
|
||||
--
|
||||
-- ★ 0001~0010 을 이미 적용한 DB 에는 이 파일이 **마지막에** 돈다(기록에 없으므로).
|
||||
-- 그래서 전부 존재 검사로 감싼다 — 그런 DB 에는 place·company 스키마가 없어 아무것도 안 한다.
|
||||
--
|
||||
-- ★ 규칙은 94551af 그대로다. 주인은 그 회사의 **가장 먼저 만든 계정**이고, 주인을 못 찾은 업장은
|
||||
-- 지운다 — 스코프가 없으면 아무에게도 안 보이는 유령이다.
|
||||
-- 실측(킹서버 2026-09-11): 업장 32곳 모두 회사에 계정이 하나씩 있어 삭제 0건.
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='place' AND table_name='places' AND column_name='company_id') THEN
|
||||
UPDATE place.places p
|
||||
SET owner_user_id = (
|
||||
SELECT u.user_id FROM company.users u
|
||||
WHERE u.company_id = p.company_id AND u.deleted = FALSE
|
||||
ORDER BY u.created_at LIMIT 1)
|
||||
WHERE p.owner_user_id IS NULL;
|
||||
DELETE FROM place.places WHERE owner_user_id IS NULL;
|
||||
ALTER TABLE place.places ALTER COLUMN owner_user_id SET NOT NULL;
|
||||
ALTER TABLE place.places DROP COLUMN company_id;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE IF EXISTS company.users DROP COLUMN IF EXISTS company_id;
|
||||
DROP TABLE IF EXISTS company.companies;
|
||||
10
postgres-init/migrations/0011_area_contents_status_index.sql
Normal file
10
postgres-init/migrations/0011_area_contents_status_index.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- 0011 · area_contents 상태 조회 인덱스 — init.sql 에만 있고 옛 DB 에는 없던 것
|
||||
--
|
||||
-- ★ 어떻게 찾았나 (2026-09-11)
|
||||
-- 킹서버 덤프 복원본에 0000~0010 을 돌린 DB 와 init.sql 로 새로 세운 DB 를 나란히 찍어 비교했다
|
||||
-- (0009 가 적은 방법 그대로). 컬럼·표는 전부 같았고 이 인덱스 하나만 새 DB 에만 있었다 —
|
||||
-- 킹서버 DB 가 이 인덱스가 init.sql 에 들어가기 전에 세워졌기 때문이다.
|
||||
-- ★ 동작에는 영향이 없다. 수집 상태로 area_contents 를 거를 때 시퀀셜 스캔이 될 뿐이다.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_local_contents_status ON public.area_contents (status, collected_at DESC)
|
||||
WHERE deleted = FALSE;
|
||||
@ -17,6 +17,8 @@ TourAPI 가 주변 정보를 받아 와도 저장할 곳이 없어 축제·맛
|
||||
## 규칙
|
||||
|
||||
- 파일명 `NNNN_한글_요약.sql` — 번호는 이어 붙인다. 지운 번호를 재사용하지 않는다.
|
||||
★ 예외는 `0000_drop_companies` 하나다 — 이 폴더가 생기기 전(09-08) 변경을 뒤늦게 옮긴 것이라
|
||||
0005 보다 앞에 둔다. 이미 전부 적용한 DB 에는 마지막에 돌므로 존재 검사로 감싸 두었다(파일 머리주석).
|
||||
- **재실행 안전하게 쓴다**(`IF NOT EXISTS` · `ADD COLUMN IF NOT EXISTS`).
|
||||
적용 기록이 있어도 사람이 손으로 한 번 더 돌릴 수 있다.
|
||||
- 한 파일 = 한 가지 변경. 여러 테이블을 건드려도 목적이 하나면 한 파일이다.
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
collectJsonLd,
|
||||
homeMeta,
|
||||
readBakedLastmod,
|
||||
readBakedNoindex,
|
||||
readBakedTitle,
|
||||
renderHead,
|
||||
renderLlmsTxt,
|
||||
@ -766,9 +767,11 @@ function writeRootMachineFiles(outRoot: string, origin: string) {
|
||||
.map((entry) => ({slug: entry.name, file: join(sitesDir, entry.name, 'index.html')}))
|
||||
// index.html 이 없으면 발행이 끝나지 않은(또는 실패한) 디렉토리다. 사이트맵에 넣지 않는다.
|
||||
.filter((entry) => existsSync(entry.file))
|
||||
.map((entry) => {
|
||||
// 제목과 lastmod 가 같은 HTML 에서 나온다 — 파일은 한 번만 읽는다.
|
||||
const html = readFileSync(entry.file, 'utf-8');
|
||||
// 제목·lastmod·noindex 가 같은 HTML 에서 나온다 — 파일은 한 번만 읽는다.
|
||||
.map((entry) => ({...entry, html: readFileSync(entry.file, 'utf-8')}))
|
||||
// ★ noindex 를 선언한 페이지(목업·백업)는 싣지 않는다 — readBakedNoindex 주석 참조.
|
||||
.filter((entry) => !readBakedNoindex(entry.html))
|
||||
.map(({html, ...entry}) => {
|
||||
return {
|
||||
// ★ 끝 슬래시를 붙이지 않는다. 페이지의 canonical 은 `/s/<slug>` 다(shared/lib/slug.ts
|
||||
// publishUrl). 사이트맵이 `/s/<slug>/` 로 어긋나 있던 동안 서치콘솔은 제출한 URL 을
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import {MOONLIGHT_STAY_PAYLOAD} from '../fixtures/moonlight-stay';
|
||||
import {readBakedLastmod, readBakedTitle} from './directory';
|
||||
import {readBakedLastmod, readBakedNoindex, readBakedTitle} from './directory';
|
||||
import {renderHead} from './head';
|
||||
import {homeMeta} from './meta';
|
||||
|
||||
@ -44,3 +44,20 @@ describe('readBakedTitle', () => {
|
||||
expect(readBakedTitle('<html><head></head></html>')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readBakedNoindex', () => {
|
||||
it('발행본 head 는 index 다 — 사이트맵에 남는다', () => {
|
||||
expect(readBakedNoindex(bakedHead())).toBe(false);
|
||||
});
|
||||
|
||||
it('noindex 를 선언한 목업은 걸러진다', () => {
|
||||
const mockup =
|
||||
'<meta name="robots" content="noindex, follow, max-snippet:-1, max-image-preview:large" />';
|
||||
expect(readBakedNoindex(mockup)).toBe(true);
|
||||
expect(readBakedNoindex('<meta name="robots" content="none" />')).toBe(true);
|
||||
});
|
||||
|
||||
it('robots 메타가 없으면 색인 대상으로 친다', () => {
|
||||
expect(readBakedNoindex('<html><head><title>x</title></head></html>')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -20,6 +20,21 @@ export function readBakedTitle(html: string): string {
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지가 스스로 `noindex` 를 선언했나. 그런 페이지는 사이트맵·`/s` 목록·llms.txt 에 싣지 않는다.
|
||||
*
|
||||
* ★ 왜 — `out/s/` 에는 payload 없는 목업·백업(`stay2` · `stay3` · `*.old`)이 섞여 있고, 목록은
|
||||
* 디렉토리를 훑어 만든다. 목업은 운영본(`/s/stay`)의 복제라 제목·본문이 같은데 canonical 은
|
||||
* 각자 자기를 가리켰다 — 구글은 같은 글 여러 벌 중 하나만 고르고, 실측(2026-09-11) 운영본이
|
||||
* "스테이머뭄" 검색에서 통째로 빠졌다. 목업에 noindex 를 박는 것만으로는 절반이다 — 사이트맵에
|
||||
* 남아 있으면 서치콘솔이 "제출된 URL 에 noindex" 오류를 계속 띄운다.
|
||||
* ★ 슬러그 이름 규칙(`.old` 등)으로 거르지 않는다 — 페이지 자신의 선언이 유일한 출처다.
|
||||
*/
|
||||
export function readBakedNoindex(html: string): boolean {
|
||||
const match = /<meta name="robots" content="([^"]*)"/.exec(html);
|
||||
return match ? /(^|[\s,])(noindex|none)([\s,]|$)/.test(match[1]) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이트맵 `lastmod` — **페이지가 스스로 선언한 `dateModified` 를 그대로** 쓴다.
|
||||
*
|
||||
|
||||
Loading…
Reference in New Issue
Block a user