실제 제작 소스에서 찾은 질문을 독립 합성 코드로 다시 만들었다. 전체 구현과 실행 예제, 테스트를 함께 공개하며 원래 업체의 프로그램과 자료는 연결하지 않는다.
정규화한 세그먼트를 공통 입력으로 쓴다
route는 한글을 NFC로 정규화하고 세그먼트마다 인코딩해 상대 경로를 만든다. 이미 인코딩한 주소나 슬래시, 역슬래시와 쿼리 문자는 받지 않는다. 사용자의 모든 URL을 정리하는 파서가 아니라 생성할 문서 이름의 작은 계약이다. 같은 정규화 경로가 두 번 나오면 manifest가 거절한다.
출력 파일은 같은 문서 세그먼트의 읽을 수 있는 경로에 index.html을 붙인다. 실제 지역 목록과 업체 도메인은 없다. 특정 운영체제의 파일 이름 규칙까지 모두 검증하는 기능은 아니므로 실제 출력기는 자신의 파일 시스템 제약도 확인해야 한다.
파일과 사이트맵의 누락을 따로 찾는다
reconcile은 예상 파일과 전달받은 출력 목록을 비교하고 public 문서만 사이트맵 예상에 넣는다. missing과 unexpected를 각각 남기며 출력 목록의 중복도 거절한다. 두 목록의 개수가 같다는 이유로 맞다고 보지 않는다. 어떤 항목이 달랐는지 읽을 수 있어야 수정할 곳을 찾을 수 있다.
함수는 실제 디렉터리를 스캔하지 않는다. 목록이 실제 빌드 결과에서 왔는지 호출자가 확인해야 한다. HTTP 상태와 캐시, 리디렉션을 검수하는 기능도 아니다. 배포 후 실제 주소 검사는 별도의 결과로 남겨야 한다.
보관과 비공개는 다른 선언이다
archive는 검색 목록에서 빠지지만 인증을 요구하지 않는다. private는 인증이 필요하다고 선언한다. noindex는 접근 통제와 이용 권한, 검색 결과의 즉시 삭제를 보장하지 않는다. 이 작은 모듈이 헤더와 로그인 설정을 배포하는 것도 아니다.
아래 기록은 합성 지역 문서와 참고 보관본, 비공개 문서의 목록을 만든 실제 로컬 결과다. 원래 업체의 보관 주소와 원본 자료는 없다. 적용할 때는 공개 정책과 서버의 강제 동작, 자료 사용 범위를 서로 다른 항목으로 확인해야 한다.
코드와 구현 살펴보기
독립 합성 전체 구현
"""Relative synthetic routes; no crawler, server or search engine operation."""
import unicodedata
from urllib.parse import quote,unquote
def route(parts):
if not isinstance(parts,list) or not 1<=len(parts)<=8: raise ValueError('bounded segments required')
cleaned=[]
for s in parts:
if not isinstance(s,str) or not s or len(s)>80:raise ValueError('bounded text segment')
s=unicodedata.normalize('NFC',s)
if s in ('.','..') or any(c in s for c in '/\\?#%') or any(ord(c)<32 or ord(c)==127 for c in s):raise ValueError('unsafe segment')
cleaned.append(s)
return '/'+ '/'.join(quote(s,safe='-._~') for s in cleaned)+'/'
def manifest(rows):
result=[];seen=set()
for r in rows:
if set(r)!={'segments','audience'} or r['audience'] not in ('public','archive','private'):raise ValueError('route contract')
url=route(r['segments'])
if url in seen:raise ValueError('duplicate normalized route')
seen.add(url);aud=r['audience']
result.append(dict(path=url,file=unquote(url).lstrip('/')+'index.html',audience=aud,indexable=aud=='public',sitemap=aud=='public',authentication_required=aud=='private'))
return result
def reconcile(rows,files,sitemap):
m=manifest(rows);expected_files={r['file'] for r in m};expected_urls={r['path'] for r in m if r['sitemap']}
if len(files)!=len(set(files)) or len(sitemap)!=len(set(sitemap)):raise ValueError('duplicate output entries')
return dict(missing_files=sorted(expected_files-set(files)),unexpected_files=sorted(set(files)-expected_files),missing_sitemap=sorted(expected_urls-set(sitemap)),unexpected_sitemap=sorted(set(sitemap)-expected_urls))같은 개수의 잘못된 결과도 찾는다
예상 공개 경로가 하나인데 사이트맵에 보관본 하나만 있으면 개수는 맞지만 결과는 틀리다. 누락과 초과를 별도로 남기면 이 교체를 발견할 수 있다. 파일 목록과 검색 목록도 같은 방식으로 각각 대조하며 한 목록의 성공을 다른 목록의 근거로 대신하지 않는다.
이 반례는 실행 기록과 테스트로 다시 확인할 수 있다. 다른 업무에 적용할 때는 이름만 바꾸지 말고 입력의 의미와 실패 정책이 같은지 먼저 확인해야 한다. 작은 독립 함수는 운영 프로그램 전체를 대신하지 않는다.
공개 코드와 ZIP은 같은 검토 파일을 사용한다. 실제 업체의 자료를 가져오거나 외부 시스템을 실행하지 않은 상태에서도 핵심 판단을 확인할 수 있도록 만들었다. 새로운 연결을 추가하면 그 연결의 결과는 별도로 기록해야 한다.
직접 확인한 실행과 남겨 둔 경계
소스 폴더와 ZIP을 새로 푼 폴더에서 각각 8개 테스트가 통과했다. 실행 예제의 실제 반환값도 records.json으로 남겼다. 공개 페이지의 코드는 내려받기 구현과 대조할 수 있다. 과거 프로젝트의 완료 보고와 테스트 수치를 이번 실행의 증거로 가져오지 않았다.
아래 선택기는 실제 로컬 실행에서 저장한 결과를 읽는다. Python이나 서버를 브라우저에서 실행하는 기능은 아니다. 이 검수는 실제 브라우저의 렌더링과 물리적 기기 조작을 포함하지 않는다. 실서비스 통합의 결과는 별도 근거로 확인해야 한다.
내려받아 적용하기 전에
ZIP에는 전체 코어와 테스트, 실행기, 저장된 결과와 사용 안내가 있다. 라이브 예제는 두 언어 HTML도 포함한다. 수정과 재사용이 가능한 새 합성 코드이며 고객 원본 이미지, 실데이터와 계정 정보는 없다. 실제 프로그램을 실행하는 지시와 외부 전송도 넣지 않았다.
적용할 때는 먼저 단위와 입력 출처, 실패한 경우의 안내를 실제 업무에 맞춰 정해야 한다. 테스트 통과가 모든 입력 사실과 운영 권한을 보장하지는 않는다. 필요한 인증·저장·전송과 화면 검수는 작은 계약 바깥의 일로 남긴다. 나는 재사용할 코드와 함께 어떤 판단을 다시 해야 하는지도 읽을 수 있게 공개한다.