글을 내려도 관련 글이나 예전 주소로 본문이 열리면 공개 상태를 바꾼 일이 끝나지 않는다. 나는 글의 목록, 관련 연결, 직접 읽기와 생성 파일을 같은 공개 기준으로 확인하려 한다. 비공개 근거까지 전달되지 않는지도 함께 본다.
현재 소스의 공개 목록 처리와 이 홈페이지의 서버 상태 검사를 읽고 작은 재사용 모듈로 정리했다. 이 모듈의 합성 검사와 실제 초안의 HTTP 차단을 구별해 기록한다. 독자가 숨겨진 카드와 보호된 원고의 차이를 알고 적용할 수 있게 하는 내용이다.
목록에서 빠졌다는 사실만으로 원고가 보호되지는 않는다
목록 화면에서 초안을 제외해도 그 본문이 공개 파일에 들어 있으면 파일을 직접 읽을 수 있다. CSS로 숨긴 글과 서버가 제공하지 않는 글은 다르다. 나는 메뉴나 카드에 보이는 조건과 실제 본문 전달 조건을 따로 확인한다. 비공개 자료를 공개 JavaScript 묶음에 넣지 않는 조건도 필요하다.
확인한 도구형 소스는 공개 상태의 메타데이터에서 목록과 관련 글을 만들고 빌드 단계에서도 발행 상태를 검사한다. 그 구조는 공개 자료의 연결을 맞추는 데 쓴다. 민감한 초안 보호가 필요한 서버에서는 본문 요청과 저장소의 상태를 직접 확인해야 한다.
관련 글도 현재 공개 목록에서 찾는다
관련 글에 적힌 식별자가 존재한다고 모두 연결하면 초안이나 내려간 글, 없는 글로 이어질 수 있다. 현재 도구형 상세 코드는 공개 글 목록에서 관련 식별자를 찾아 남은 항목만 표시한다. 생성기에는 관련 식별자가 공개 목록에 있는지 확인하는 조건도 있다.
새 모듈은 현재 글 자신과 중복, 모르는 식별자, 비공개 상태를 관련 목록에서 제외한다. 표시할 제목과 설명도 공개 항목의 투영에서 가져온다. 원래 초안의 제목을 링크 없이 보여주는 방식으로 처리하지 않는다. 비공개라는 상태와 공개할 수 있는 정보의 범위를 같이 다룬다.
직접 주소의 본문 요청에도 같은 상태를 적용한다
목록에서는 보이지 않아도 주소를 알고 들어오는 요청이 있을 수 있다. 이 홈페이지의 서버는 직접 상세 주소에서 원고의 상태와 종류를 확인하고 공개되지 않은 항목을 404로 처리한다. 관리자 미리보기는 인증된 별도 흐름으로 제공한다. 목록을 거치지 않아도 공개 경계가 유지되어야 한다.
새 모듈의 직접 로더 예시는 서버 저장소를 조회한 뒤 공개 상태인 본문만 돌려준다. 브라우저 안의 모의 저장소로 같은 코드를 돌려보는 아래 예시는 조건 설명이다. 실제 서버 인증이나 원고의 비밀을 클라이언트 함수 하나로 보호했다는 뜻은 아니다.
공개 객체는 필요한 필드만 새로 만든다
저장소의 객체 전체를 공개 응답으로 보내고 나중에 화면에서 일부만 표시하면 내부 근거와 설정이 함께 전달될 수 있다. 이 CMS는 공개 원고와 비공개 근거를 저장할 때 구분하고 공개 전달에서는 근거를 제외한다. 화면에 없다는 것보다 응답에 없다는 조건이 중요하다.
예시의 목록은 제목과 요약, 식별자만 새로 만들고 직접 본문도 허용한 문자열 배열만 복사한다. 원래 객체의 private 같은 추가 필드는 따라가지 않는다. 실제 블록과 미디어를 쓰는 편집실은 더 넓은 공개 스키마를 검토해야 하며 이 작은 예시가 모든 블록 검증을 대체하지 않는다.
현재 발행 상태를 사용하는 구현
다음 함수는 공개 목록과 관련 목록, 서버 저장소의 직접 본문 로딩을 분리한다. 입력의 상태를 명시적으로 요구하고 중복 식별자를 거절한다. 가져온 공개 본문을 다른 곳에서 수정해도 저장소의 배열이 바뀌지 않도록 새 배열로 반환한다.
이번에는 합성 저장소로 9개 검사를 실행했다. 목록의 필드 투영, 관련 항목의 제외 조건, 직접 초안·내림 차단, 모르는 주소, 공개 본문의 비공개 필드 제외, 잘못된 상태, 중복 식별자, 본문 복사, 새 조회에서의 상태 변경을 확인했다. 배포된 서버의 동작은 별도로 HTTP에서 확인했다.
코드와 구현 살펴보기
JavaScript · 공개 목록·관련 글·직접 본문의 경계
function validEntry(row){
if(!row||!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(row.slug||'')||!['published','draft','withdrawn'].includes(row.status)||typeof row.title!=='string'||typeof row.summary!=='string')throw new TypeError('Explicit entry identity and publication state required');
}
function publicSummary(row){return {slug:row.slug,title:row.title,summary:row.summary};}
export function publicCatalog(rows){
if(!Array.isArray(rows))throw new TypeError('Entry array required');const seen=new Set();
for(const row of rows){validEntry(row);if(seen.has(row.slug))throw new TypeError('Duplicate entry');seen.add(row.slug);}
return rows.filter(row=>row.status==='published').map(publicSummary);
}
export function publicRelated(rows,currentSlug,requested){
if(!Array.isArray(requested))throw new TypeError('Related identifiers required');
const bySlug=new Map(publicCatalog(rows).map(row=>[row.slug,row])),seen=new Set(),result=[];
for(const slug of requested){if(slug===currentSlug||seen.has(slug)||!bySlug.has(slug))continue;seen.add(slug);result.push({...bySlug.get(slug)});}
return result;
}
export async function loadPublishedEntry(slug,store){
if(typeof slug!=='string'||!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))return null;
const row=await store.find(slug);if(!row)return null;validEntry(row);
if(row.status!=='published')return null;
if(row.slug!==slug||!Array.isArray(row.body)||row.body.some(x=>typeof x!=='string'))throw new TypeError('Validated published body required');
return {...publicSummary(row),body:[...row.body]};
}
실제 초안 차단과 합성 예시의 범위를 나눈다
현재 이 작업의 비공개 검수용 원고가 초안 상태인지 관리자 API에서 읽고, 두 언어의 직접 주소가 404인지 확인했다. 공개 목록과 RSS, 사이트맵에서도 그 원고가 빠진 7개 검사가 통과했다. 검수용 원고를 바꾸거나 발행하지 않는 읽기 확인이었다.
아래는 모든 내용을 새로 만든 합성 데이터로 상태를 바꿔보는 실행 예시다. 실제 비공개 자료가 들어 있지 않다. 이 프레임의 표시 결과와 실제 서버의 접근 차단을 혼동하지 않는다. 공개 응답의 예시는 확인하지만 기존 Studio Chrome을 유지하므로 새 화면·클릭 검수는 수행하지 않았다.