사진 아래에 설명이 이어지는 카드에서는 이미지의 끝과 카드의 끝이 다르다. 나는 원래 제작 기록의 고지 위치 수정에서 이 관계를 떼어내 새 모듈로 만들었다. 이미지 전용 부모에 표시를 연결하고 설명은 그 밖에 두어, 어느 자료에 대한 문구인지 유지하려는 구현이다.

공유 코드는 입력한 자료 종류와 생성 여부, 표시 문구를 계산한다. 실제 이미지를 읽어 AI 생성 여부나 사실을 알아내지는 않는다. 아래 그림은 새 코드로 만든 합성 도식이며 실제 고객 사진이 아니다. 설명 길이와 도식 비율, 선언 상태를 바꾸어 작은 연결의 동작을 직접 살펴볼 수 있다.

표시의 기준은 사진만 들어 있는 부모다

attachImageNotice를 연결할 영역에는 직접 자식으로 IMG, PICTURE 또는 SVG 하나만 둔다. 사진 설명이나 제목까지 들어 있으면 연결 전에 거절한다. 이것은 모든 기존 홈페이지 구조를 자동으로 고치는 프로그램이 아니라, 연결할 사람이 이미지 전용 영역을 명시하는 계약이다. 다른 내용은 바깥 카드나 캡션에 둔다.

별도 CSS는 그 부모를 위치 기준으로 삼고 표시를 모서리에 놓는다. 사진을 채우거나 자를지 선택하는 역할은 맡지 않는다. 실제 원본의 비율과 표시 방식을 유지한 채 고지 영역만 연결한다. 설명이 늘어나도 그 글자가 부모 영역에 포함되지 않으므로 표시의 기준이 카드 끝으로 이동하지 않는다.

선언 검사가 실제 자료의 사실 검사는 아니다

입력에는 kind, generated, disclosure 세 항목만 넣는다. kind는 photo, document, logo 중 하나고 생성 여부는 Boolean이다. 생성 사진이면 비어 있지 않은 문구를 요구한다. 생성 문서와 로고는 이 모듈의 선택한 규칙에서 거절한다. 생성하지 않은 자료에는 빈 문구를 받아 표시를 숨긴다.

이 판단은 입력된 선언에 대한 것이다. 실제 파일 종류, 사용 권한이나 실제 공간의 모습이 맞는지 검사하지 않는다. 같은 picture에 PC·모바일 자료가 섞인 경우에도 이 코드가 자동으로 어느 파일이 보이는지 알아내지 않는다. 연결할 때 실제 선택된 자료와 확인한 선언을 맞춰 update를 호출해야 한다.

설명을 늘리며 위치를 확인하는 작동 예제

예제에서 설명을 늘려 보거나 이미지 비율을 바꾸어 표시가 어디에 붙어 있는지 볼 수 있다. 원본 문서 선언으로 바꾸면 표시가 사라지고, 생성 문서로 선언하려는 조작은 기존 상태를 유지하며 거절된다. 같은 그림이 실제로 생성 사진이나 공식 문서라는 뜻은 아니다. 선언과 표현의 연결을 살피는 합성 예제다.

표시 문구를 바꾸는 입력은 textContent로 연결한다. 태그 모양의 문자열을 넣어도 HTML 요소를 새로 만들지 않는다. 문구는 짧고 자료의 역할을 분명히 하도록 작성한다. 최대 입력 길이가 허용된다고 모든 실제 사진과 작은 영역에서 좋은 읽힘이 보장되는 것은 아니므로, 실제 크기와 바탕을 함께 확인한다.

새 모듈을 추가로 검사하다가 허용 길이240자의 문구가 실제 좁은 페이지에서 사진 높이172px보다 큰191px로 표시되는 문제를 찾았다. 기본 문구의 통과와 최대 입력의 검사는 달랐다. 수정 코드는 문구 전체와 입력 한도를 보존하면서 표기 영역의 높이를 사진 안으로 제한한다. 긴 내용은 초점을 두고 PageDown·End 같은 키로 스크롤해 읽을 수 있게 했다.

합성 도식과 명시적 선언으로 부모 영역·표시 상태를 확인합니다.

새 코드와 재사용 파일

아래 코드는 ZIP의 image_notice.mjs와 같다. 선언을 계산하는 함수와 DOM 연결을 나누었다. 입력 선언을 바꾸는 update는 먼저 검증한 뒤 표시를 바꾼다. 잘못된 입력이 들어와도 이전 정상 표시를 바로 지우지 않는다. 같은 영역에 다시 연결할 때는 기존 소유 표기를 정리해 하나의 표시를 유지한다.

ZIP에는 코드, CSS, 한국어·영어 예제, Node 검사와 사용 안내의 여섯 파일이 들어 있다. node checks.mjs로 포함 검사를 실행한다. 예제는 폴더를 로컬 HTTP로 제공해 여는 실행 방법을 안내에 적었다. 원래 고객 템플릿이나 사진을 넣은 묶음이 아니며 외부 서버로 값을 보내는 코드도 없다.

코드와 구현 살펴보기

새 독립 JavaScript · 명시적 선언과 사진 전용 표기

// New declared-metadata rule and visual-only notice connector.
const mounted = new WeakMap();

export function noticeFromDeclaration(metadata) {
  if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata) ||
      Object.keys(metadata).sort().join() !== 'disclosure,generated,kind' ||
      !['photo','document','logo'].includes(metadata.kind) ||
      typeof metadata.generated !== 'boolean' || typeof metadata.disclosure !== 'string' ||
      metadata.disclosure.length > 240) throw new TypeError('Invalid image declaration');
  if (metadata.generated && metadata.kind !== 'photo') throw new TypeError('Generated factual material is outside this module');
  const text = metadata.disclosure.trim();
  if ((metadata.generated && !text) || (!metadata.generated && text)) throw new TypeError('Declaration and notice disagree');
  return Object.freeze({kind:metadata.kind,visible:metadata.generated,text});
}

export function attachImageNotice(surface, metadata) {
  const next = noticeFromDeclaration(metadata);
  if (!surface?.children || !surface.ownerDocument || !surface.classList) throw new TypeError('Visual surface required');
  const old = mounted.get(surface);
  const children = [...surface.children].filter(node => node !== old?.element);
  if (children.length !== 1 || !['IMG','PICTURE','SVG'].includes(children[0].tagName.toUpperCase())) {
    throw new TypeError('Surface must contain only one visual, with captions outside');
  }
  old?.destroy();
  const hadClass = surface.classList.contains('image-notice-surface');
  const label = surface.ownerDocument.createElement('span');
  label.className = 'image-notice';
  label.tabIndex = 0;
  let active = true, current = next;
  const render = value => { label.textContent=value.text; label.hidden=!value.visible; };
  surface.classList.add('image-notice-surface'); surface.append(label); render(current);
  const api = Object.freeze({
    element:label,
    get notice() { return current; },
    update(value) {
      if (!active) throw new Error('Notice is detached');
      const valid = noticeFromDeclaration(value);
      current=valid; render(valid); return valid;
    },
    destroy() {
      if (!active) return;
      active=false; label.remove();
      if (!hadClass) surface.classList.remove('image-notice-surface');
      if (mounted.get(surface)===api) mounted.delete(surface);
    }
  });
  mounted.set(surface,api); return api;
}
고지 코드·CSS·한영 예제·25개 검사·사용 안내 ZIP

해제할 때도 원래 자료를 지킨다

destroy는 이 연결이 만든 표시를 없애고 원래의 클래스 소속을 복원한다. 사진 요소의 정체와 원래 속성은 바꾸지 않는다. 오래된 객체를 해제해 새 연결의 표시까지 지우지 않게 하고, 이미 해제된 연결의 update는 거절한다. 기존 사진의 연결을 보존하는 것과 새 표시를 관리하는 책임을 분리했다.

이번 스물다섯 코드 검사는 선언의 잘못된 조합, 입력 보존, 사진 전용 영역, 텍스트 처리와 갱신·재연결·해제를 합성 DOM으로 확인했다. 실제 사진의 고지 읽힘이나 생성 출처, 원래 홈페이지 전체의 검수는 아니다. 이 공개 예제의 실제 브라우저 위치와 입력 동작도 그 코드 검사와 따로 확인할 대상이다.

사이트에 붙일 때 남겨야 할 확인

사이트의 실제 자료를 조사하고, 그 자료에 맞는 선언을 정하는 일은 연결 전에 한다. 이미지가 바뀌거나 responsive 소스가 바뀌면 같은 선언이 계속 맞는지도 본다. 사진과 캡션의 구조를 확인하고, 표기가 충분히 읽히며 사진의 필요한 내용과 조작을 가리지 않는지 살핀다. 이 모듈의 투명한 스타일이 모든 사진의 정답은 아니다.

고지 문구는 그대로 글자로 남기므로 고객 식별 정보를 지우는 기능도 아니다. 공개 예제에는 일반 문구를 사용하고 실제 자료의 공개 범위는 따로 정한다. 내가 재사용하도록 남기려는 것은 사진의 출처를 추정하는 자동 판정이 아니라, 확인한 설명을 올바른 시각 영역에 전달하는 작고 검사 가능한 책임이다.