Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
483 lines
29 KiB
HTML
483 lines
29 KiB
HTML
<!-- ABAP LLM 위키 뷰어 템플릿. 플레이스홀더는 DATA_JSON / GENERATED_AT 를 밑줄 두 개로 감싼 토큰.
|
|
이 주석에는 그 토큰도, HTML 주석 닫기 기호(하이픈 두 개 + 꺾쇠)도 적지 말 것 — 치환된 데이터가
|
|
주석 안에 박히고 ABAP 주석 화살표가 이 주석을 조기에 닫아 화면이 깨진 적이 있다 (2026-09-17).
|
|
빌드: python -m wiki_out.viewer [--out path] / 라이브: GET /wiki-viewer -->
|
|
<title>ABAP LLM 위키</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
|
<style>
|
|
:root{
|
|
--bg:#F6F6F2; --surface:#FFFFFF; --surface2:#EFEFE9; --ink:#232527; --muted:#6C7075;
|
|
--line:#E2E2DA; --accent:#0E7490; --accent-deep:#0B5D74; --accent-bg:#E3F0F4;
|
|
--track:#E6E6DF; --ok:#15803D; --ok-bg:#E5F3E9; --warn:#96560F; --warn-bg:#F7EDDD;
|
|
--code-bg:#F0F0EA; --shadow:0 1px 3px rgba(35,37,39,.07);
|
|
}
|
|
@media (prefers-color-scheme: dark){
|
|
:root:not([data-theme="light"]){
|
|
--bg:#17181A; --surface:#1F2124; --surface2:#26282C; --ink:#E8E8E4; --muted:#9DA1A6;
|
|
--line:#33363B; --accent:#4FB8D3; --accent-deep:#7FCBDE; --accent-bg:#1C2B31;
|
|
--track:#2C2F33; --ok:#6FBE86; --ok-bg:#1E2E23; --warn:#D9A05B; --warn-bg:#322718;
|
|
--code-bg:#26282C; --shadow:0 1px 3px rgba(0,0,0,.35);
|
|
}
|
|
}
|
|
:root[data-theme="dark"]{
|
|
--bg:#17181A; --surface:#1F2124; --surface2:#26282C; --ink:#E8E8E4; --muted:#9DA1A6;
|
|
--line:#33363B; --accent:#4FB8D3; --accent-deep:#7FCBDE; --accent-bg:#1C2B31;
|
|
--track:#2C2F33; --ok:#6FBE86; --ok-bg:#1E2E23; --warn:#D9A05B; --warn-bg:#322718;
|
|
--code-bg:#26282C; --shadow:0 1px 3px rgba(0,0,0,.35);
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--bg);color:var(--ink);
|
|
font:400 14px/1.65 "IBM Plex Sans KR",'Apple SD Gothic Neo','Malgun Gothic',sans-serif;}
|
|
.mono{font-family:"IBM Plex Mono",Consolas,monospace}
|
|
a{color:var(--accent-deep)}
|
|
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0;text-align:left}
|
|
button:focus-visible,a:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}
|
|
|
|
header.app{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;
|
|
padding:14px 20px 12px;border-bottom:1px solid var(--line);background:var(--surface)}
|
|
header.app h1{margin:0;font-size:17px;font-weight:700;letter-spacing:-.01em}
|
|
header.app h1 button{font:inherit;font-weight:700;letter-spacing:inherit;cursor:pointer}
|
|
header.app h1 button:hover .accent{text-decoration:underline}
|
|
header.app h1 .accent{color:var(--accent-deep)}
|
|
header.app .sub{color:var(--muted);font-size:12px}
|
|
header.app .home-btn{margin-left:auto;font-size:12px;color:var(--accent-deep);font-weight:500}
|
|
|
|
.shell{display:grid;grid-template-columns:290px minmax(0,1fr);min-height:calc(100vh - 50px)}
|
|
@media(max-width:820px){.shell{grid-template-columns:1fr}.side{border-right:0;border-bottom:1px solid var(--line);max-height:45vh}}
|
|
|
|
/* ---------- 사이드바 ---------- */
|
|
.side{border-right:1px solid var(--line);background:var(--surface);overflow-y:auto;padding:12px 0 32px}
|
|
.side .search{margin:0 14px 10px}
|
|
.side input{width:100%;padding:7px 10px;border:1px solid var(--line);border-radius:6px;
|
|
background:var(--bg);color:var(--ink);font:inherit;font-size:13px}
|
|
.side .sec{margin:14px 0 4px;padding:0 16px;font-size:11px;font-weight:700;letter-spacing:.08em;
|
|
text-transform:uppercase;color:var(--muted)}
|
|
.side .sec-toggle{display:flex;align-items:center;gap:7px;width:100%;text-align:left;cursor:pointer}
|
|
.side .sec-toggle:hover{color:var(--ink)}
|
|
.side .sec-toggle .cnt{margin-left:auto;text-transform:none;letter-spacing:0}
|
|
.nav-item{display:flex;align-items:center;gap:7px;width:100%;padding:4px 16px;font-size:13px;line-height:1.4}
|
|
.nav-item:hover{background:var(--surface2)}
|
|
.nav-item.active{background:var(--accent-bg);color:var(--accent-deep);font-weight:500}
|
|
.nav-item .cnt{margin-left:auto;color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums}
|
|
/* 프로그램 행: ▶ 는 펼치기, 이름은 문서 열기 — 역할이 다르므로 버튼을 나눈다 */
|
|
.prog-nav{display:flex;align-items:center;gap:4px;padding:0 10px 0 12px}
|
|
.prog-nav:hover{background:var(--surface2)}
|
|
.prog-nav .tw-btn{flex:none;width:18px;height:26px;display:flex;align-items:center;justify-content:center;border-radius:4px}
|
|
.prog-nav .tw-btn:hover{background:var(--line)}
|
|
.prog-nav .nav-name{flex:1;min-width:0;padding:4px 6px;font-family:"IBM Plex Mono",monospace;font-size:12.5px;
|
|
border-radius:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.prog-nav .nav-name:hover{color:var(--accent-deep)}
|
|
.prog-nav .nav-name.active{background:var(--accent-bg);color:var(--accent-deep);font-weight:500}
|
|
.prog-nav .cnt{flex:none;color:var(--muted);font-size:11px;font-variant-numeric:tabular-nums;padding-right:4px}
|
|
.tw{display:inline-block;width:10px;color:var(--muted);font-size:10px;transition:transform .12s}
|
|
@media(prefers-reduced-motion:reduce){.tw{transition:none}}
|
|
.tw.open{transform:rotate(90deg)}
|
|
.units{margin:0;padding:0 0 4px}
|
|
.unit-item{display:flex;gap:7px;width:100%;padding:2px 16px 2px 33px;font-size:12px;color:var(--muted)}
|
|
.unit-item:hover{background:var(--surface2)}
|
|
.unit-item.active{background:var(--accent-bg);color:var(--accent-deep)}
|
|
.unit-item .nm{font-family:"IBM Plex Mono",monospace;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.utype{flex:none;width:44px;font-size:9.5px;font-weight:500;letter-spacing:.04em;color:var(--muted)}
|
|
.unit-item .cnt{margin-left:auto}
|
|
.unit-empty{padding:3px 16px 5px 33px;font-size:11px;color:var(--muted);opacity:.7}
|
|
/* 사이드바의 조각 항목 — kind 색으로 종류를 구분한다 */
|
|
.chunk-item{display:flex;gap:6px;align-items:baseline;width:100%;padding:2px 14px 2px 44px;
|
|
font-size:11.5px;color:var(--muted);text-align:left}
|
|
.chunk-item:hover{background:var(--surface2);color:var(--ink)}
|
|
.chunk-item .cp{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.45}
|
|
.chunk-item .kind{flex:none;font-size:9px;font-weight:600;letter-spacing:.03em;padding:1px 4px;
|
|
border-radius:3px;background:var(--surface2);border:1px solid var(--line);text-transform:uppercase}
|
|
.kind.k-sql_select{color:#0b6bb8;border-color:#9dc7e8}
|
|
.kind.k-db_write{color:#b0480b;border-color:#e8b79d}
|
|
.kind.k-fm_call{color:#6b3fb8;border-color:#c0a9e8}
|
|
.kind.k-validation{color:#0a7b52;border-color:#9de0c2}
|
|
.kind.k-interface{color:#b80b6b;border-color:#e89dc7}
|
|
.kind.k-aggregation,.kind.k-calculation{color:#8a6d0b;border-color:#e0d09d}
|
|
.kind.k-control_flow{color:#555;border-color:var(--line)}
|
|
h3.flash,h4.flash{animation:fl 1.4s ease-out}
|
|
@keyframes fl{0%{background:var(--accent);color:#fff}100%{background:transparent}}
|
|
.dot{flex:none;width:6px;height:6px;border-radius:50%;margin-top:6px;background:var(--track)}
|
|
.dot.done{background:var(--accent)}
|
|
.results .nav-item{align-items:flex-start}
|
|
.results .meta{font-size:11px;color:var(--muted)}
|
|
|
|
/* ---------- 본문 ---------- */
|
|
.main{padding:22px 28px 60px;min-width:0}
|
|
.wrap{max-width:860px}
|
|
|
|
/* 개요 */
|
|
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:10px;margin:6px 0 24px}
|
|
.tile{background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:12px 14px;box-shadow:var(--shadow)}
|
|
.tile .v{font-size:24px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1.15}
|
|
.tile .l{font-size:11.5px;color:var(--muted);margin-top:2px;white-space:nowrap}
|
|
.tile.hero .v{color:var(--accent-deep)}
|
|
h2.sec-h{font-size:14px;font-weight:700;margin:26px 0 10px;display:flex;align-items:baseline;gap:10px}
|
|
h2.sec-h .hint{font-weight:400;color:var(--muted);font-size:12px}
|
|
/* 프로그램 목록 — 한 줄에 하나, 타이틀은 한 줄로 자른다 */
|
|
.plist{background:var(--surface);border:1px solid var(--line);border-radius:8px;overflow:hidden;box-shadow:var(--shadow)}
|
|
.plist .ph,.plist .prow{display:grid;grid-template-columns:130px minmax(0,1fr) 150px 56px;gap:14px;align-items:center;padding:0 14px}
|
|
.plist .ph{font-size:11px;font-weight:500;color:var(--muted);letter-spacing:.03em;background:var(--surface2);
|
|
border-bottom:1px solid var(--line);height:32px}
|
|
.plist .prow{width:100%;height:40px;border-bottom:1px solid var(--line);text-align:left}
|
|
.plist .prow:last-child{border-bottom:0}
|
|
.plist .prow:hover{background:var(--accent-bg)}
|
|
.plist .pn{font-family:"IBM Plex Mono",monospace;font-size:13px;font-weight:500;color:var(--accent-deep)}
|
|
.plist .pt{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.plist .pt .draft{font-size:10px;color:var(--warn);border:1px solid var(--warn);border-radius:99px;padding:0 6px;margin-left:6px;vertical-align:1px}
|
|
.plist .num{text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;color:var(--muted)}
|
|
.meter{display:flex;align-items:center;gap:8px}
|
|
.meter .bar{display:block;flex:1;height:6px;border-radius:3px;background:var(--track);overflow:hidden}
|
|
.meter .fill{display:block;height:100%;border-radius:3px;background:var(--accent)}
|
|
.meter .lab{flex:none;width:26px;text-align:right;font-size:12.5px;font-variant-numeric:tabular-nums;font-weight:500}
|
|
@media(max-width:700px){.plist .ph,.plist .prow{grid-template-columns:110px minmax(0,1fr) 90px}.plist .num,.plist .ph .num{display:none}}
|
|
.note{font-size:12px;color:var(--muted);margin:14px 0 0}
|
|
|
|
/* 문서 뷰 */
|
|
.doc-head{border-bottom:1px solid var(--line);padding-bottom:14px;margin-bottom:18px}
|
|
.crumb{font-size:11.5px;color:var(--muted);font-family:"IBM Plex Mono",monospace;margin-bottom:6px}
|
|
.doc-head h2{margin:0 0 8px;font-size:21px;font-weight:700;letter-spacing:-.015em;text-wrap:balance}
|
|
.chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
|
|
.chip{font-size:11px;font-weight:500;padding:2px 8px;border-radius:99px;border:1px solid var(--line);color:var(--muted);background:var(--surface)}
|
|
.chip.type{color:var(--accent-deep);border-color:var(--accent);background:var(--accent-bg)}
|
|
.chip.ok{color:var(--ok);border-color:var(--ok);background:var(--ok-bg)}
|
|
.chip.warn{color:var(--warn);border-color:var(--warn);background:var(--warn-bg)}
|
|
.gen{font-size:11.5px;color:var(--muted);margin-top:8px}
|
|
.gen .mono{font-size:11px}
|
|
|
|
.md{max-width:760px}
|
|
.md h2{font-size:16px;font-weight:700;margin:24px 0 8px;padding-top:14px;border-top:1px solid var(--line)}
|
|
.md h2:first-child{border-top:0;padding-top:0;margin-top:8px}
|
|
.md h3{font-size:14px;font-weight:700;margin:18px 0 6px}
|
|
.md p{margin:8px 0;max-width:70ch}
|
|
.md ul,.md ol{margin:8px 0;padding-left:22px}
|
|
.md li{margin:3px 0}
|
|
.md code{font-family:"IBM Plex Mono",monospace;font-size:12.5px;background:var(--code-bg);
|
|
padding:1px 5px;border-radius:4px}
|
|
.md blockquote{margin:10px 0;padding:6px 12px;border-left:3px solid var(--warn);
|
|
background:var(--warn-bg);border-radius:0 6px 6px 0;color:var(--ink);font-size:13px}
|
|
.md .tbl{overflow-x:auto;margin:10px 0;border:1px solid var(--line);border-radius:8px}
|
|
.md table{border-collapse:collapse;width:100%;font-size:12.5px}
|
|
.md th{text-align:left;font-weight:500;color:var(--muted);font-size:11.5px;letter-spacing:.03em;
|
|
padding:7px 12px;border-bottom:1px solid var(--line);background:var(--surface2);white-space:nowrap}
|
|
.md td{padding:6px 12px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
.md tr:last-child td{border-bottom:0}
|
|
.md td:first-child{font-family:"IBM Plex Mono",monospace;font-size:12px;white-space:nowrap}
|
|
.md a.doc-link{color:var(--accent-deep);text-decoration:none;border-bottom:1px dashed var(--accent)}
|
|
.md a.doc-link:hover{border-bottom-style:solid}
|
|
.md a.dead{color:var(--muted);border-bottom:1px dotted var(--line);text-decoration:none;cursor:default}
|
|
.md a.jump{color:var(--accent-deep);text-decoration:none;border-bottom:1px dashed var(--accent);cursor:pointer}
|
|
.md a.jump:hover{border-bottom-style:solid;background:var(--accent-bg,var(--surface2))}
|
|
.empty{color:var(--muted);padding:40px 0;text-align:center}
|
|
|
|
/* ---- 로직 조각: 소스 원문 + 설명 ---- */
|
|
.md{max-width:980px}
|
|
.md h4.chunk-h{font-size:13.5px;font-weight:600;margin:20px 0 8px;padding:8px 12px;
|
|
background:var(--accent-bg,var(--surface2));border-left:3px solid var(--accent);
|
|
border-radius:0 6px 6px 0;line-height:1.5}
|
|
.md h4.decl-h{font-size:13px;font-weight:600;margin:18px 0 6px;padding:5px 10px;
|
|
background:var(--surface2);border-left:3px solid var(--track,var(--line));border-radius:0 6px 6px 0;
|
|
font-family:"IBM Plex Mono",monospace;scroll-margin-top:12px}
|
|
.md pre.code{margin:0 0 10px;padding:10px 0;background:var(--code-bg);border:1px solid var(--line);
|
|
border-radius:8px;overflow-x:auto}
|
|
.md pre.code code{display:block;background:none;padding:0 14px;border-radius:0;
|
|
font-family:"IBM Plex Mono",monospace;font-size:12px;line-height:1.65;white-space:pre;tab-size:2}
|
|
.md pre.code .ln{display:inline-block;min-width:62px;color:var(--muted);opacity:.65;
|
|
user-select:none;text-align:right;padding-right:10px}
|
|
.md pre.code .k{color:var(--accent-deep);font-weight:600}
|
|
.md pre.code .s{color:#0a7b52}
|
|
.md pre.code .c{color:var(--muted);font-style:italic}
|
|
@media (prefers-color-scheme:dark){
|
|
.md pre.code .s{color:#5fd4a0}
|
|
}
|
|
</style>
|
|
|
|
<header class="app">
|
|
<h1><button id="homeTitle" title="개요로 돌아가기"><span class="accent">ABAP</span> LLM 위키</button></h1>
|
|
<span class="sub">스냅샷 __GENERATED_AT__</span>
|
|
<button class="home-btn" id="homeBtn">개요로</button>
|
|
</header>
|
|
<div class="shell">
|
|
<nav class="side" id="side" aria-label="위키 탐색">
|
|
<div class="search"><input id="q" type="search" placeholder="프로그램·unit·태그 검색" aria-label="검색"></div>
|
|
<div id="tree"></div>
|
|
</nav>
|
|
<main class="main"><div class="wrap" id="view"></div></main>
|
|
</div>
|
|
|
|
<script type="application/json" id="data">__DATA_JSON__</script>
|
|
<script>
|
|
const D = JSON.parse(document.getElementById('data').textContent);
|
|
const view = document.getElementById('view');
|
|
const tree = document.getElementById('tree');
|
|
const esc = s => String(s??'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
|
|
/* ABAP 간이 하이라이트 — ' 123| CODE' 형태의 줄번호를 분리해 표시한다 */
|
|
const ABAP_KW = new RegExp('\\b('+[
|
|
'SELECT','FROM','INTO','TABLE','WHERE','AND','OR','NOT','APPENDING','FOR','ALL','ENTRIES',
|
|
'INSERT','UPDATE','MODIFY','DELETE','COMMIT','ROLLBACK','WORK','APPEND','COLLECT','READ',
|
|
'LOOP','ENDLOOP','AT','IF','ELSEIF','ELSE','ENDIF','CASE','WHEN','ENDCASE','DO','ENDDO',
|
|
'WHILE','ENDWHILE','CHECK','EXIT','CONTINUE','RETURN','LEAVE','FORM','ENDFORM','PERFORM',
|
|
'CALL','FUNCTION','METHOD','ENDMETHOD','MODULE','ENDMODULE','EXPORTING','IMPORTING','CHANGING',
|
|
'TABLES','EXCEPTIONS','USING','DATA','TYPES','CONSTANTS','CLEAR','REFRESH','FREE','SORT',
|
|
'MOVE-CORRESPONDING','CONCATENATE','DESCRIBE','LINES','AUTHORITY-CHECK','OBJECT','ID','FIELD',
|
|
'MESSAGE','WITH','DISPLAY','LIKE','TYPE','VALUE','SPACE','INITIAL','IS','EQ','NE','GT','LT',
|
|
'DESTINATION','TRANSPORTING','KEY','INDEX','BY','OF','TO','ENDSELECT','SY-SUBRC'
|
|
].join('|')+')\\b','g');
|
|
function hlAbap(raw){
|
|
const m = raw.match(/^(\s*\d+\|)(.*)$/);
|
|
const no = m ? m[1] : '';
|
|
let code = m ? m[2] : raw;
|
|
code = esc(code);
|
|
/* 주석을 먼저 떼낸다 — 주석 안의 ABAP 키워드는 칠하지 않는다.
|
|
전체주석(줄 첫 문자 '*')과 인라인주석('"' → esc 후 ")을 나눠 처리한다. */
|
|
let comment = '';
|
|
if (/^\s*\*/.test(code)) { comment = code; code = ''; }
|
|
else {
|
|
const q = code.indexOf('"');
|
|
if (q >= 0) { comment = code.slice(q); code = code.slice(0, q); }
|
|
}
|
|
code = code.replace(/('|')([^']*)('|')/g, '<span class="s">$1$2$3</span>')
|
|
.replace(ABAP_KW, '<span class="k">$1</span>');
|
|
return (no?`<span class="ln">${esc(no)}</span>`:'') + code +
|
|
(comment?`<span class="c">${comment}</span>`:'');
|
|
}
|
|
|
|
/* ---------- 미니 마크다운 렌더러 ---------- */
|
|
function inline(s){
|
|
s = esc(s);
|
|
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, t, href) => {
|
|
if (href.startsWith('/')) {
|
|
const p = href.replace(/^\//,'').replace(/\/$/,'');
|
|
const key = D.docs[p] ? p : (D.docs[p + '.md'] ? p + '.md' : null);
|
|
return key ? `<a class="doc-link" href="#" data-doc="${esc(key)}">${t}</a>`
|
|
: `<a class="dead" title="문서 없음(스냅샷 미포함)">${t}</a>`;
|
|
}
|
|
/* 문서 안 앵커(#decl-...) — 조각의 정의부 이름에서 아래 선언 원문으로 뛴다 */
|
|
if (href.startsWith('#')) return `<a class="jump" href="${esc(href)}" data-jump-id="${esc(href.slice(1))}">${t}</a>`;
|
|
return `<a class="dead">${t}</a>`;
|
|
});
|
|
return s;
|
|
}
|
|
/* 제목 끝의 `{#id}` = 앵커 표기. 마크다운으로 읽을 때는 잡음이 적고, 여기서는 id 가 된다. */
|
|
const HEAD_ID=/\s*\{#([^}\s]+)\}\s*$/;
|
|
function headId(text){
|
|
const m=text.match(HEAD_ID);
|
|
return m ? [text.slice(0, m.index), m[1]] : [text, ''];
|
|
}
|
|
function renderMd(src){
|
|
const out=[]; const lines=src.split('\n'); let i=0, list=null;
|
|
const closeList=()=>{ if(list){out.push(list==='ul'?'</ul>':'</ol>'); list=null;} };
|
|
while(i<lines.length){
|
|
const ln=lines[i];
|
|
/* 코드펜스 — 로직 조각의 소스 원문. 이게 없으면 코드가 한 줄씩 <p> 로 흩어진다. */
|
|
if(/^```/.test(ln)){
|
|
closeList();
|
|
const lang=ln.slice(3).trim(); const buf=[]; i++;
|
|
while(i<lines.length && !/^```/.test(lines[i])){ buf.push(lines[i]); i++; }
|
|
i++; /* 닫는 펜스 */
|
|
out.push(`<pre class="code${lang?' lang-'+esc(lang):''}"><code>`+
|
|
buf.map(l=>hlAbap(l)).join('\n')+'</code></pre>');
|
|
continue;
|
|
}
|
|
if(/^\s*$/.test(ln)){ closeList(); i++; continue; }
|
|
if(/^####\s/.test(ln)){ closeList();
|
|
const [txt,id]=headId(ln.slice(5));
|
|
const cls=id.startsWith('decl-')?'decl-h':'chunk-h'; /* 정의부 항목은 조각 제목과 다른 모양 */
|
|
out.push(`<h4 class="${cls}"${id?` id="${esc(id)}"`:''}>`+inline(txt)+'</h4>'); i++; continue; }
|
|
if(/^###\s/.test(ln)){ closeList();
|
|
const [txt,id]=headId(ln.slice(4));
|
|
out.push(`<h3${id?` id="${esc(id)}"`:''}>`+inline(txt)+'</h3>'); i++; continue; }
|
|
if(/^##\s/.test(ln)){ closeList(); out.push('<h2>'+inline(ln.slice(3))+'</h2>'); i++; continue; }
|
|
if(/^>\s?/.test(ln)){ closeList(); out.push('<blockquote>'+inline(ln.replace(/^>\s?/,''))+'</blockquote>'); i++; continue; }
|
|
if(/^\|/.test(ln)){
|
|
closeList(); const rows=[];
|
|
while(i<lines.length && /^\|/.test(lines[i])){ rows.push(lines[i]); i++; }
|
|
const cells=r=>r.replace(/^\||\|$/g,'').split('|').map(c=>c.trim());
|
|
let html='<div class="tbl"><table>';
|
|
const head=cells(rows[0]); const hasSep=rows[1]&&/^[\s|:-]+$/.test(rows[1]);
|
|
html+='<thead><tr>'+head.map(c=>'<th>'+inline(c)+'</th>').join('')+'</tr></thead><tbody>';
|
|
for(let r=hasSep?2:1; r<rows.length; r++)
|
|
html+='<tr>'+cells(rows[r]).map(c=>'<td>'+inline(c)+'</td>').join('')+'</tr>';
|
|
out.push(html+'</tbody></table></div>'); continue;
|
|
}
|
|
if(/^[-*]\s/.test(ln)){ if(list!=='ul'){closeList(); out.push('<ul>'); list='ul';}
|
|
out.push('<li>'+inline(ln.replace(/^[-*]\s/,''))+'</li>'); i++; continue; }
|
|
if(/^\d+\.\s/.test(ln)){ if(list!=='ol'){closeList(); out.push('<ol>'); list='ol';}
|
|
out.push('<li>'+inline(ln.replace(/^\d+\.\s/,''))+'</li>'); i++; continue; }
|
|
closeList(); out.push('<p>'+inline(ln)+'</p>'); i++;
|
|
}
|
|
closeList(); return out.join('\n');
|
|
}
|
|
|
|
/* ---------- 화면: 개요 ---------- */
|
|
function showHome(){
|
|
setActive(null);
|
|
const s=D.stats;
|
|
const maxc = Math.max(1, ...D.programs.map(p=>p.chunks||0));
|
|
let h=`<div class="tiles">
|
|
<div class="tile"><div class="v">${s.programs}</div><div class="l">프로그램</div></div>
|
|
<div class="tile hero"><div class="v">${(s.chunks||0).toLocaleString()}</div><div class="l">로직 조각</div></div>
|
|
<div class="tile"><div class="v">${(s.units_total||0).toLocaleString()}</div><div class="l">조각 있는 unit</div></div>
|
|
<div class="tile"><div class="v">${(s.tables||0)}</div><div class="l">테이블</div></div>
|
|
<div class="tile"><div class="v">${(s.functions||0)}</div><div class="l">펑션</div></div>
|
|
<div class="tile"><div class="v">${s.human_verified}</div><div class="l">사람 검토</div></div>
|
|
</div>
|
|
<h2 class="sec-h">프로그램 <span class="hint">행을 누르면 인덱싱 결과(요약 · 로직 조각)를 봅니다</span></h2>
|
|
<div class="plist">
|
|
<div class="ph"><span>프로그램</span><span>타이틀</span><span>로직 조각</span><span class="num">unit</span></div>`;
|
|
for(const p of D.programs){
|
|
const c = p.chunks||0;
|
|
const w = Math.round(100*c/maxc);
|
|
const draft = p.status==='draft' ? '<span class="draft">초안</span>' : '';
|
|
h+=`<button class="prow" data-doc="${esc(p.path)}" title="${esc(p.desc||p.title)}">
|
|
<span class="pn">${esc(p.name)}</span>
|
|
<span class="pt">${esc(p.title)}${draft}</span>
|
|
<span class="meter"><span class="bar"><span class="fill" style="width:${w}%"></span></span><span class="lab">${c}</span></span>
|
|
<span class="num">${p.total}</span></button>`;
|
|
}
|
|
h+=`</div>
|
|
<p class="note">로직 조각 = LLM 이 코드에서 골라낸 업무 로직 단위 (소스 원문 + 설명). 테이블·호출은 파서가 뽑은 사실이며,
|
|
"어디서 채워지나 · 누가 호출하나"는 trace / call-graph / usage 도구로 확인합니다.</p>`;
|
|
view.innerHTML=h;
|
|
}
|
|
|
|
/* ---------- 화면: 문서 ---------- */
|
|
function showDoc(path){
|
|
const d=D.docs[path]; if(!d){ view.innerHTML='<div class="empty">문서를 찾을 수 없습니다</div>'; return; }
|
|
setActive(path);
|
|
const chips=[`<span class="chip type">${esc(d.t)}</span>`];
|
|
if(d.ver) chips.push('<span class="chip ok">사람 검토됨</span>');
|
|
if(d.st==='draft') chips.push('<span class="chip warn">초안 — 요약 전/재검토</span>');
|
|
for(const t of (d.tags||[])) chips.push(`<span class="chip">${esc(t)}</span>`);
|
|
let h=`<div class="doc-head"><div class="crumb">${esc(path)}</div>
|
|
<h2>${esc(d.title||path)}</h2><div class="chips">${chips.join('')}</div>
|
|
<div class="gen">생성: <span class="mono">${esc(d.gen||'?')}</span>${d.res?` · <span class="mono">${esc(d.res)}</span>`:''}</div></div>
|
|
<article class="md">${renderMd(d.body)}</article>`;
|
|
view.innerHTML=h; view.scrollIntoView({block:'start'});
|
|
}
|
|
|
|
/* ---------- 사이드바 ---------- */
|
|
function navBtn(cls,doc,inner){ return `<button class="${cls}" data-doc="${esc(doc)}">${inner}</button>`; }
|
|
function buildTree(){
|
|
let h='<div class="sec">프로그램</div>';
|
|
for(const p of D.programs){
|
|
/* ▶ 는 unit·조각 목록 펼치기, 이름은 프로그램 문서(인덱싱 결과) 열기 */
|
|
h+=`<div class="prog-nav">
|
|
<button class="tw-btn" data-toggle="${esc(p.name)}" aria-expanded="false" title="unit · 조각 펼치기"><span class="tw" id="tw-${esc(p.name)}">▶</span></button>
|
|
<button class="nav-name" data-doc="${esc(p.path)}" data-expand="${esc(p.name)}" title="${esc(p.title)}">${esc(p.name)}</button>
|
|
<span class="cnt" title="로직 조각 수">${p.chunks||0}</span></div>
|
|
<div class="units" id="u-${esc(p.name)}" hidden></div>`;
|
|
}
|
|
if(D.packages.length){ h+='<div class="sec">패키지</div>';
|
|
for(const k of D.packages) h+=navBtn('nav-item',k.path,`<span>${esc(k.name)}</span><span class="cnt">${k.count}</span>`); }
|
|
if(D.concepts.length){ h+='<div class="sec">개념</div>';
|
|
for(const c of D.concepts) h+=navBtn('nav-item',c.path,`<span>${esc(c.name)}</span>`); }
|
|
/* 테이블·펑션은 수백 건이라 접어 둔다 — 펼치면 프로그램 목록을 밀어내기 때문 */
|
|
h+=entitySection('tables','테이블',D.tables,12);
|
|
h+=entitySection('functions','펑션',D.functions,11.5);
|
|
tree.innerHTML=h;
|
|
}
|
|
function entitySection(key,label,items,fs){
|
|
if(!items||!items.length) return '';
|
|
return `<button class="sec sec-toggle" data-esec="${key}" aria-expanded="false">
|
|
<span class="tw" id="tw-e-${key}">▶</span>${label}<span class="cnt">${items.length}</span></button>
|
|
<div class="units" id="e-${key}" hidden data-fs="${fs}"></div>`;
|
|
}
|
|
function fillEntities(key){
|
|
const box=document.getElementById('e-'+key); if(box.dataset.filled) return;
|
|
const items=D[key]||[]; const fs=box.dataset.fs;
|
|
box.innerHTML=items.map(t=>
|
|
`<button class="unit-item" data-doc="${esc(t.path)}"><span class="nm" style="font-size:${fs}px">${esc(t.name)}</span></button>`
|
|
).join(''); box.dataset.filled='1';
|
|
}
|
|
/* 프로그램을 펼치면 그 프로그램의 unit·조각 목록을 보여준다.
|
|
조각은 별 문서가 아니라 프로그램 문서 안의 섹션이므로, 클릭 시 해당 섹션으로 스크롤한다. */
|
|
function fillUnits(name){
|
|
const box=document.getElementById('u-'+name); if(box.dataset.filled) return;
|
|
const p=D.programs.find(x=>x.name===name); let h='';
|
|
if(!p.units.length) h='<div class="unit-empty">조각 없음 — 추출 전</div>';
|
|
for(const u of p.units){
|
|
h+=`<button class="unit-item" data-doc="${esc(p.path)}" data-jump="${esc(u.name)}">
|
|
<span class="utype">${esc(u.type)}</span><span class="nm">${esc(u.name)}</span>
|
|
<span class="cnt">${u.chunks.length}</span></button>`;
|
|
for(const c of u.chunks)
|
|
h+=`<button class="chunk-item" data-doc="${esc(p.path)}" data-jump="${esc(c.purpose)}">
|
|
<span class="kind k-${esc(c.kind)}">${esc(c.kind)}</span>
|
|
<span class="cp">${esc(c.purpose)}</span></button>`;
|
|
}
|
|
box.innerHTML=h; box.dataset.filled='1';
|
|
}
|
|
function setActive(path){
|
|
for(const el of document.querySelectorAll('.nav-item.active,.unit-item.active,.nav-name.active')) el.classList.remove('active');
|
|
if(!path) return;
|
|
const btn=[...tree.querySelectorAll('[data-doc]')].find(b=>b.dataset.doc===path);
|
|
if(btn){
|
|
btn.classList.add('active');
|
|
const box=btn.closest('.units');
|
|
if(box){ box.hidden=false; const tw=document.getElementById('tw-'+box.id.slice(2)); if(tw) tw.classList.add('open'); }
|
|
}
|
|
}
|
|
|
|
/* ---------- 검색 ---------- */
|
|
const idx = Object.entries(D.docs).map(([p,d])=>({p, s:(d.title+' '+(d.d||'')+' '+(d.tags||[]).join(' ')).toLowerCase()}));
|
|
document.getElementById('q').addEventListener('input', e=>{
|
|
const q=e.target.value.trim().toLowerCase();
|
|
if(!q){ buildTree(); return; }
|
|
const hits=idx.filter(x=>x.s.includes(q)).slice(0,60);
|
|
let h=`<div class="sec">검색 결과 ${hits.length}건${hits.length===60?'+':''}</div><div class="results">`;
|
|
for(const {p} of hits){ const d=D.docs[p];
|
|
h+=navBtn('nav-item',p,`<span><span style="display:block">${esc(d.title)}</span><span class="meta">${esc(d.t)} · ${esc(p)}</span></span>`); }
|
|
tree.innerHTML=h+'</div>';
|
|
});
|
|
|
|
/* ---------- 이벤트 ---------- */
|
|
/* 문서 안 앵커로 이동 — 뒤로 돌아올 수 있게 브라우저 히스토리에도 남긴다 */
|
|
function jumpTo(id){
|
|
const el=document.getElementById(id);
|
|
if(!el) return;
|
|
el.scrollIntoView({block:'start', behavior:'smooth'});
|
|
el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'), 1400);
|
|
history.replaceState(null, '', '#'+id);
|
|
}
|
|
document.addEventListener('click', e=>{
|
|
const j=e.target.closest('[data-jump-id]');
|
|
if(j){ e.preventDefault(); jumpTo(j.dataset.jumpId); return; }
|
|
const t=e.target.closest('[data-toggle]');
|
|
if(t){ const n=t.dataset.toggle; fillUnits(n);
|
|
const box=document.getElementById('u-'+n); box.hidden=!box.hidden;
|
|
t.setAttribute('aria-expanded', String(!box.hidden));
|
|
document.getElementById('tw-'+n).classList.toggle('open', !box.hidden); return; }
|
|
const es=e.target.closest('[data-esec]');
|
|
if(es){ const k=es.dataset.esec; fillEntities(k);
|
|
const box=document.getElementById('e-'+k); box.hidden=!box.hidden;
|
|
es.setAttribute('aria-expanded', String(!box.hidden));
|
|
document.getElementById('tw-e-'+k).classList.toggle('open', !box.hidden); return; }
|
|
const b=e.target.closest('[data-doc]');
|
|
if(b){
|
|
e.preventDefault();
|
|
showDoc(b.dataset.doc);
|
|
/* 사이드바에서 프로그램 이름을 눌렀으면 그 프로그램의 unit·조각 목록도 펼쳐 둔다 */
|
|
const ex=b.dataset.expand;
|
|
if(ex){ fillUnits(ex); const box=document.getElementById('u-'+ex);
|
|
if(box){ box.hidden=false; document.getElementById('tw-'+ex).classList.add('open');
|
|
const tb=tree.querySelector(`[data-toggle="${CSS.escape(ex)}"]`); if(tb) tb.setAttribute('aria-expanded','true'); } }
|
|
/* 조각은 별 문서가 아니라 프로그램 문서 안의 섹션이다 — 해당 제목으로 스크롤한다 */
|
|
const jump=b.dataset.jump;
|
|
if(jump){
|
|
const want=jump.trim().toLowerCase();
|
|
const hs=[...view.querySelectorAll('h3,h4')];
|
|
const hit=hs.find(h=>h.textContent.trim().toLowerCase().includes(want));
|
|
if(hit){ hit.scrollIntoView({block:'start'}); hit.classList.add('flash');
|
|
setTimeout(()=>hit.classList.remove('flash'),1400); }
|
|
}
|
|
}
|
|
});
|
|
document.getElementById('homeBtn').addEventListener('click', showHome);
|
|
document.getElementById('homeTitle').addEventListener('click', ()=>{ showHome(); window.scrollTo({top:0}); });
|
|
|
|
buildTree(); showHome();
|
|
</script>
|