feat: 문서 구조 변경 및 tech-visual 스킬 추가

This commit is contained in:
DongHyeonka
2026-09-04 18:20:00 +09:00
parent 43901f0abf
commit 2efb7ee1f2
683 changed files with 61180 additions and 10479 deletions
@@ -0,0 +1,49 @@
죽은 링크 전수 감사 — 서버가 내보내는 모든 주소
출처: https://hyeonworks.com — 2026-09-04 실행
스크립트: evidence/audit/link-audit.py (재실행하면 같은 방식으로 다시 검사한다)
이 감사가 필요한 이유: 주소는 서버가 게시할 때 만들어 DB 에 저장한 문자열이라 화면
코드 어디에도 흔적이 없다. 저장소 안의 to=/href= 리터럴만 훑는 감사로는 잡히지
않는다 — 실제로 결정 링크가 그렇게 숨어 있었다(§9.2).
$ python3 evidence/audit/link-audit.py
======================================================================
200 /cases/bff-session-csrf-responsibility
200 /cases/collection-fetch-join-in-memory-paging
200 /cases/eager-toone-nplus1-without-access
200 /cases/fetch-join-multibag-and-row-explosion
200 /cases/identity-header-trust
200 /cases/spa-browser-credential-boundary
200 /cases/split-custody-access-token
200 /concepts/idp-brokering
200 /projects/backend-clean-architecture
200 /projects/keycloak-patterns
200 /projects/keycloak-patterns/decisions#bff-owns-token-when-browser-must-not
200 /projects/liner-n-plus-1
200 /questions/bff-session-authorized-client-store
200 /questions/edge-authorization-scope
200 /questions/refresh-rotation-replica-contention
200 /questions/server-session-pattern-multi-instance
200 /references/authorization-code-endpoint-credential-movement
200 /references/bff-authentication-design-criteria
200 /references/external-idp-federation-application-boundary
200 /references/forward-auth-identity-header-trust
200 /references/oauth-oidc-pattern-selection-criteria
200 /references/oauth-token-application-session-boundary
200 /references/public-confidential-client-boundary
200 /releases/0.1.0
200 /releases/0.2.0
200 /releases/0.3.0
200 /topics/jpa-feed-query-performance
200 /topics/jpa-feed-query-performance/derived-query
200 /topics/jpa-feed-query-performance/fetch-join
200 /topics/jpa-feed-query-performance/fetch-join-paging
200 /topics/oauth-oidc-auth-boundary
200 /topics/oauth-oidc-auth-boundary/bff
200 /topics/oauth-oidc-auth-boundary/forward-auth
200 /topics/oauth-oidc-auth-boundary/mediator
200 /topics/oauth-oidc-auth-boundary/spa
검사한 주소 35개
죽은 링크 없음
종료코드: 0
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""서버가 내보내는 모든 공개 주소를 모아 각각에 GET 을 보낸다.
주소는 게시 시점에 서버가 만들어 DB(public_resource_projection.navigation_path)에
저장한 문자열이다. 그래서 저장소 안의 `to=` / `href=` 리터럴만 훑는 감사로는 잡히지
않는다 — 실제로 결정 링크가 그렇게 숨어 있었다.
사용법: python3 link-audit.py [BASE] (기본 https://hyeonworks.com)
"""
import json, re, sys, urllib.request
from collections import defaultdict
BASE = sys.argv[1] if len(sys.argv) > 1 else "https://hyeonworks.com"
def get(path):
try:
with urllib.request.urlopen(BASE + path, timeout=20) as r:
return json.loads(r.read())
except Exception as e:
return {"__error__": str(e)}
def status(path):
# 앵커와 질의 문자열은 라우트를 고르지 않는다 — 떼고 확인한다.
target = path.split("#")[0].split("?")[0]
try:
with urllib.request.urlopen(BASE + target, timeout=20) as r:
return r.status
except Exception as e:
return getattr(e, "code", "ERR")
paths = defaultdict(set) # path -> 어디서 나왔나
def collect(node, origin):
if isinstance(node, dict):
for key in ("path", "canonicalPath", "projectPath"):
value = node.get(key)
if isinstance(value, str) and value.startswith("/"):
paths[value].add(origin)
for value in node.values():
collect(value, origin)
elif isinstance(node, list):
for value in node:
collect(value, origin)
# 1) 목록에서 시작한다
for seed in ("/api/v1/public/home", "/api/v1/public/topics", "/api/v1/public/projects",
"/api/v1/public/knowledge", "/api/v1/public/questions", "/api/v1/public/releases"):
collect(get(seed), seed)
# 2) 문서 상세를 전부 돈다 — 관계는 상세에만 있다
SEGMENTS = ("cases", "references", "questions", "concepts")
for path in [p for p in list(paths) if re.match(rf"^/({'|'.join(SEGMENTS)})/[^/]+$", p)]:
segment, slug = path.strip("/").split("/", 1)
collect(get(f"/api/v1/public/{segment}/{slug}"), path)
# 3) 프로젝트의 하위 목록
for path in [p for p in list(paths) if re.match(r"^/projects/[^/]+$", p)]:
slug = path.rsplit("/", 1)[-1]
for sub in ("", "/decisions", "/records", "/activity"):
collect(get(f"/api/v1/public/projects/{slug}{sub}"), path + sub)
# 4) 주제 허브와 축 — 목록 응답에 path 가 없어 위 크롤이 닿지 않는다
for topic in (get("/api/v1/public/topics").get("data") or {}).get("items", []):
paths[f"/topics/{topic['slug']}"].add("listPublicTopics")
detail = (get(f"/api/v1/public/topics/{topic['slug']}").get("data") or {})
for variant in (detail.get("variants") or []):
if variant.get("path"):
paths[variant["path"]].add(f"/topics/{topic['slug']}")
bad = []
for path in sorted(paths):
code = status(path)
print(f" {code} {path}")
if code != 200:
bad.append((code, path, sorted(paths[path])[:2]))
print(f"\n검사한 주소 {len(paths)}")
if not bad:
print("죽은 링크 없음")
else:
for code, path, origin in bad:
print(f" DEAD {code} {path}{origin}")
sys.exit(1 if bad else 0)