자료를 많이 모았다고 바로 좋은 글이 되는 것은 아니다. 같은 사건의 자료를 묶고, 독자가 궁금해할 질문을 고르고, 초안이 사용할 사실을 확인해야 한다. 나는 K-POP PULSE의 초기 구조와 운영 기록에서 이 연결을 읽고, 다른 홈페이지에서도 직접 살펴볼 수 있는 작은 편집 보드를 새로 만들었다.

이 모듈은 당시 프로그램을 복원한 것이 아니다. 합성 자료로 근거 연결의 빈틈을 드러내는 공유 예제다. 독립 HTML 화면으로 열거나 핵심 함수만 자신의 프로그램에 가져갈 수 있다. 실제 출처 확인, 원고 저장, 발행 승인은 운영 서비스에서 따로 구현해야 한다.

어떤 상황에서 쓸 수 있는가

여러 안내문에서 한 편의 글을 만들거나, 자료가 추가될 때 원고의 근거를 다시 정리해야 하는 상황에 쓸 수 있다. 한 자료에 날짜가 있고 다른 자료에 장소가 있다면 함께 연결할 수 있다. 변경 공지에 다른 날짜가 나오면 서로 다른 값이 있다는 신호를 먼저 보여 준다.

하지만 다른 값이 있다는 이유만으로 어느 자료가 옳은지 자동 선택하지는 않는다. 정정인지, 서로 다른 행사인지, 입력 실수인지 읽어 보는 일이 남는다. 나는 이 예제에서 필요한 확인이 빠져 있다는 신호와 편집자가 내려야 할 판단을 구별했다.

화면에서 바꾸어 볼 순서

처음에는 일정 안내와 장소 안내가 연결되어 있지만 출처를 확인하지 않은 상태다. 예제의 검토 가정을 선택하면 초안용 사실표를 볼 수 있다. 날짜 변경 안내를 추가하면 날짜가 충돌한다. 이전 일정 안내를 현재 선택에서 빼면 변경된 날짜와 장소로 사실표를 다시 구성할 수 있다.

선택에서 빼는 것은 원본 자료를 삭제하는 일이 아니다. catalog에는 모든 합성 자료가 그대로 남고, 이번 원고에 사용할 연결만 달라진다. 출처 확인 체크도 예제의 가정이다. 체크를 누르는 행동이 실제 자료의 진실성을 확인해 주지 않는다.

합성 자료를 사용하는 새 예제. 실제 저장·발행·출처 검증 기능은 없다.

자료와 원고의 연결을 바꾸지 않고 읽는 함수

핵심 함수는 catalog의 자료와 issue의 sourceIds, requiredFacts를 입력으로 받는다. 연결된 자료를 찾고 각 사실에 sourceId와 관측 시점을 붙여 돌려준다. 같은 연결을 두 번 넣어도 사실을 두 번 늘리지 않으며 입력 자료는 수정하지 않는다.

없는 자료가 연결되었는지, 미검토 자료가 남았는지, 필요한 필드가 빠졌는지, 같은 필드에 다른 값이 있는지를 구별한다. 마지막 상태의 뜻은 초안에 넘길 표를 만들 수 있다는 것이다. 완성된 기사이거나 공개해도 된다는 뜻이 아니다.

코드와 구현 살펴보기

새로 작성한 설명용 핵심 함수. 현재 원본 프로젝트의 코드가 아니다.

// A new teaching module derived from a source -> issue -> draft workflow.
// It links evidence and reports mechanical gaps. It cannot verify truth or publish.
export function assessIssue(catalog, issue) {
  if (!Array.isArray(catalog) || !issue || !Array.isArray(issue.sourceIds) ||
      !Array.isArray(issue.requiredFacts)) throw new TypeError('Invalid issue contract');
  const byId = new Map();
  for (const source of catalog) {
    if (!source || typeof source.id !== 'string' || !source.id || byId.has(source.id) ||
        typeof source.title !== 'string' || !Array.isArray(source.facts) ||
        typeof source.observedAt !== 'string' || !Number.isFinite(Date.parse(source.observedAt)) ||
        !['unchecked', 'checked'].includes(source.review)) throw new TypeError('Invalid source record');
    for (const fact of source.facts) {
      if (!fact || typeof fact.key !== 'string' || !fact.key ||
          typeof fact.value !== 'string' || !fact.value.trim()) throw new TypeError('Invalid fact');
    }
    byId.set(source.id, source);
  }
  if (issue.sourceIds.some(id => typeof id !== 'string') ||
      issue.requiredFacts.some(key => typeof key !== 'string' || !key)) throw new TypeError('Invalid links');
  const ids = [...new Set(issue.sourceIds)];
  const missingSources = ids.filter(id => !byId.has(id));
  const selected = ids.filter(id => byId.has(id)).map(id => byId.get(id));
  const pendingSources = selected.filter(source => source.review !== 'checked').map(source => source.id);
  const facts = selected.flatMap(source => source.facts.map(fact => ({
    key: fact.key, value: fact.value, sourceId: source.id, observedAt: source.observedAt
  })));
  const keys = new Set(facts.map(fact => fact.key));
  const missingFacts = [...new Set(issue.requiredFacts)].filter(key => !keys.has(key));
  const conflicts = [...keys].filter(key => new Set(facts.filter(fact => fact.key === key).map(fact => fact.value)).size > 1);
  const state = !ids.length || missingSources.length ? 'missing-source' :
    conflicts.length ? 'conflict' : pendingSources.length ? 'needs-source-review' :
    missingFacts.length ? 'missing-fact' : 'ready-for-draft';
  return {state, sourceIds: ids, missingSources, pendingSources, missingFacts, conflicts, facts};
}

내 사이트에 맞게 바꿀 부분

예제의 필수 항목은 날짜와 장소다. 제품 글이라면 모델명과 사양, 작업 기록이라면 변경 대상과 확인 결과 등으로 바꿀 수 있다. 어떤 필드가 필요할지는 글의 질문에서 정한다. 모든 글에 같은 사실표를 강제하지 않는다.

각 source의 review에는 실제 출처 검토 결과를 연결해야 한다. 사용자 체크 하나를 곧바로 검증 완료로 받아들이는 운영 구조가 되어서는 안 된다. 문서 버전과 검토자, 검토 시점, 적용 범위는 실제 서비스에서 보존하고 현재 원고의 연결을 바꿀 때도 이전 자료를 남긴다.

  1. catalog: 원본의 식별자, 제목, 관측 시점, 사실 항목과 검토 상태
  2. sourceIds: 이번 이슈에 사용할 근거의 연결
  3. requiredFacts: 이 글의 질문에 답하기 위해 꼭 필요한 항목
  4. labels와 CSS: 방문자의 언어와 화면 구성에 맞춘 표현

같은 HTML 한 벌을 PC와 작은 화면에 배치한다

자료 목록과 판단 결과를 두 영역으로 두고, 작은 폭에서는 읽는 순서대로 한 줄로 쌓도록 CSS를 작성했다. 목록을 PC용과 모바일용으로 두 벌 만들지 않는다. 자료 선택은 기본 체크박스를 쓰고 상태 안내는 별도 영역에서 갱신한다. 상태가 바뀐다고 선택 중인 입력을 통째로 다시 만들지 않도록 했다.

아래 CSS와 내려받는 독립 HTML을 함께 사용할 수 있다. 이번에는 실제 Studio 브라우저에서 한국어·영어 예제 프레임을 각각 960px·390px 폭으로 열어 자료 선택, 확인, 충돌과 사실표 표시를 조작했다. 작은 폭에서는 패널이 한 열로 이어지고 가로 넘침이 없었다. 이 확인은 예제 프레임의 동작이며 실제 휴대폰이나 다른 사이트의 전체 화면을 검수한 결과는 아니다. 적용할 본문 폭과 글자 크기는 자신의 사이트에서 다시 확인한다.

코드와 구현 살펴보기

독립 HTML에 포함된 새 레이아웃 CSS. 사이트 본문 폭에 맞게 수정할 수 있다.


:root{color-scheme:light;font:16px/1.55 system-ui,sans-serif;color:#202b32;background:#f6f8f7}
*{box-sizing:border-box}body{margin:0;padding:24px}h1{font-size:clamp(24px,4vw,34px);line-height:1.2;margin:0 0 10px}
h2{font-size:20px;margin:0 0 18px}p{margin:0 0 16px}small{color:#526268}.layout{display:grid;gap:20px;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.panel{min-width:0;padding:20px;background:white;border:1px solid #d5ded9;border-radius:14px}.source{display:flex;gap:12px;margin-bottom:12px;padding:14px;border:1px solid #d5ded9;border-radius:10px;cursor:pointer}
.source input{margin-top:7px;flex:none}.source span{min-width:0}.source strong,.source small{display:block}.review{display:flex;gap:10px;align-items:start;margin-top:18px}
.review input{margin-top:6px}input{width:19px;height:19px;accent-color:#176b4f}button{font:inherit;padding:10px 15px;border:0;border-radius:8px;background:#176b4f;color:white;cursor:pointer;max-width:100%}
button:disabled{background:#63756c;cursor:not-allowed;opacity:.7}button:focus-visible,input:focus-visible{outline:3px solid #eda842;outline-offset:4px}
.status{padding:12px;border-left:4px solid #176b4f;background:#eff7f2;font-weight:650}ul{padding-left:22px}li{margin-bottom:9px;overflow-wrap:anywhere}pre{font:13px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;padding:14px;background:#eff2f0;border-radius:8px}
.boundary{font-size:14px;color:#526268;margin-top:18px}[hidden]{display:none!important}@media(max-width:680px){body{padding:16px}.layout{grid-template-columns:minmax(0,1fr)}.panel{padding:16px}}

확인한 범위와 서비스에 추가할 일

새 핵심 함수는 근거 없음, 알 수 없는 연결, 날짜 충돌, 필수 항목 누락, 미검토 자료, 중복 연결, 잘못된 자료 계약과 입력 불변성을 포함한 로컬 합성 검사를 통과했다. 이것은 함수가 정한 계약을 지켰다는 검사다. 실제 출처의 사실, 브라우저의 화면 조작, 운영 데이터의 저장을 검사한 결과는 아니다.

실제 운영에 넣을 때는 인증과 수정 이력, 서버 저장, 발행 전 검토를 연결해야 한다. catalog나 원고 전체를 공개 JavaScript에 넣으면 그 내용도 방문자에게 전달된다. 비공개 자료를 사용하는 관리 보드라면 서버가 접근을 제한하고 공개 본문에는 필요한 결과만 보내는 별도의 경계가 필요하다.

내려받는 묶음에는 한국어·영어 독립 화면, 핵심 함수, 재현 검사와 사용 설명이 들어 있다. 자신의 홈페이지에 복사하고 수정해 사용할 수 있다. 작업을 끝내기 전에는 실제 화면에서 자료 선택, 충돌 표시, 필수 정보 누락과 사실표 출력까지 확인하면 좋다.

편집 보드 HTML·코드·검사·사용 설명 내려받기
새 공유 예제 ZIP. 한국어·영어 독립 화면 포함.