- 웹UI-개선플랜-byQwen27B.md: app.js/index.html 분리 리팩토링 계획. 실코드 대조 감리 결과 §0.5 추가 — 치명 결함 3건(모듈레벨 상태/최상위 실행문 누락, 로더 기동 부재, async 파셜 배선 타이밍) 정정 및 정정 로더 포함 - 메타데이터업데이트/문서뷰어/커밋-브랜치정리 대화모음 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
24 KiB
웹UI 개선 — 코딩 실행 계획 (by Qwen27B)
작성: 2026-05-24 · 상태: 감리 정정 반영 — 실행 전 §0.5 필독 · 대상:
src/Web/wwwroot/기반:웹UI-개선플랜-byOPUS.md+diagnosis-checklist.md규칙 준수 감리: 2026-05-24 실코드 대조 — 치명 결함 3건 발견·정정(§0.5)
0. 진단 요약 (Checklist STEP 1~4 적용)
STEP 1 — 맥락 파악
| 항목 | 내용 |
|---|---|
| 파일 역할 | ASP.NET Static Files로 서빙되는 바닐라 JS SPA 프론트엔드 |
| 아키텍처 레이어 | 진입점 (UI). 백엔드 API(/api/*)에 fetch로 호출 |
| 관련 문서 | AGENTS.md(빌드/런/배포), CODING_CONVENTIONS.md(JSON camelCase 규칙) |
| 제약 | 빌드 스텝 0, 정적 파일만, dotnet publish로 배포 |
STEP 2 — 구조 탐색
wwwroot/
├── index.html (1,761줄, pane 16개 + 모달 2개) ← 초안 15개는 오기(docs=Tab16 누락)
├── js/
│ ├── app.js (5,148줄, 전 탭 로직)
│ ├── docs.js (712줄, ✅ 분리된 모범 사례) ← 계획 초안 571줄은 오기(D6)
│ ├── pid-viewer.js (P&ID 뷰어)
│ └── xlsx.full.min.js
├── css/
│ ├── style.css (2,231줄)
│ ├── docs.css
│ └── pid_graph.css
└── lib/ (uPlot 등 외부 라이브러리)
STEP 3 — 코드 읽기 (핵심 발견)
- 탭 라우터:
app.js:5-20—document.querySelectorAll('.nav-item')click listener. 진입 훅 4개:opcsvr,t2s,fast,docs - docs.js 패턴:
docsInit()진입 함수 +docsState.inited플래그로 1회 초기화. 전역esc()/kbToken재사용 - 모달 2개: fastRecord 신규 세션 모달(
index.html:1679-1724), 날짜 선택 팝업(index.html:1727-1754) — pane 외부에 위치 - datepicker:
app.js:1660-1779— 여러 탭에서 공유하는 공용 컴포넌트
STEP 4 — 호출 계층 지도
사용자 클릭 (nav-item)
→ app.js:5 click listener
→ tab 클래스 토글
→ if (tab === 'opcsvr') srvLoad() ← API 호출
→ if (tab === 't2s') t2sInitMode() ← UI 모드 설정
→ if (tab === 'fast') fastSessionsLoad() ← API 호출
→ if (tab === 'docs') docsInit() ← API 호출 (docs.js)
0.5 감리 정정 (실코드 대조 — 2026-05-24) ⚠️ 실행 전 필독
본 계획을
src/Web/wwwroot/실제 코드와 1:1 대조한 결과, 실행을 막는 치명 결함 3건 + 정합성 결함 3건 발견. 아래 정정을 반영한 후 실행할 것. 헤더/Phase 본문의 라인 번호는 대부분 정확하나, 분리 전제와 로더 코드가 틀렸다.
결함 요약
| # | 심각도 | 위치 | 문제 | 정정 |
|---|---|---|---|---|
| D1 | 🔴 치명 | §4.2, §4.3 | app.js는 함수 선언만 있지 않음 — 모듈레벨 상태변수 ~25개와 로드 시 실행되는 최상위 문(certStatus() 2593, btn-fast-*/btn-pid-* addEventListener 3001~3640)이 산재. "함수 단위 이동 + _t2sLastResult 1개만 이전"으로는 누락/오작동 |
상태변수·최상위 문 전수 이전(아래 인벤토리). DOM 배선은 init 훅 내부로 |
| D2 | 🔴 치명 | §2.2, §3.3, §5.1 | 로더에 기동 코드 부재 — loadPane/activateTab이 click에만 묶여, 페이지 로드 시 기본 활성 pane(pane-cert)이 빈 채로 남음. §3.3의 "eager 주입"도 §2.2 코드엔 없음 |
DOMContentLoaded에서 기본 pane 주입+init(아래 정정 로더) |
| D3 | 🔴 치명 | §2.2, §5.2 | pane이 async fetch 파셜이 되면, fast/pid 탭의 최상위 addEventListener 배선이 pane DOM 주입보다 먼저 실행돼 무력화(?.로 silent no-op) → 버튼 사망. eager(Phase1)에서도 fetch 비동기라 동일 발생 |
해당 배선을 fast/pid의 1회 setup 훅으로 이동 |
| D4 | 🟡 정합성 | §2.3 ↔ §4.2#3 | 로드 순서 모순 — §4.2#3 "datepicker가 hist/evt보다 먼저" vs §2.3 datepicker 맨 뒤. 실제로는 모두 function 선언(호이스팅)이라 순서 무관 |
§4.2#3 폐기, §2.3 순서 유지 |
| D5 | 🟡 정합성 | §4.1 | "원본 라인" 범위가 상호 겹침(분할표가 아님): hist 1120-1658 ⊃ evt 1349-1503; t2s 1868-2643 ⊃ core helper 2599-2643; conn의 llm* 2개(160,177)가 llmchat llm*와 충돌 |
범위는 참고용. prefix grep 기계 분리 금지, 함수명 단위 판단 |
| D6 | ⚪ 경미 | §0 STEP2, §6.1 | 수치 오기 — docs.js 571줄 → 712줄; pane 15개 → 16개(docs=Tab16); "15개 fetch" → 16개 |
정정(완료) |
실제 함수 배치 — 탭별 연속 블록이 아님 (D5 근거)
app.js 함수는 탭별로 깔끔히 연속되지 않으며 교차(interleave)된다:
- hist/ht:
1120-1332+ (evt1349-1503끼어듦) +1505-1658 - core helper
fmtTs/fmtVal/parseEnumPv:2599-2643(t2s 영역 한가운데) llmLoadConfig/llmSaveModelConfig:160/177(conn 영역 — llmchat 아님)certStatus();호출:2593(t2s 영역 — cert 아님)
→ grep '^function llm' 같은 기계 분리 금지. 함수명 단위로 소속 탭을 직접 판단할 것.
모듈레벨 상태변수 전수 인벤토리 (각 소속 파일 상단으로 이전 — D1)
| 변수 | 라인 | 소속 파일 |
|---|---|---|
_t2sLastResult |
2 | t2s.js |
_nmOffset / _nmTotal / _nmLimit |
492-494 | nm-dash.js |
PB_GROUPS / pbPreviewData / SUBAREA_OPTIONS |
613-614, 1022 | pb.js |
HIST_TAG_IDS |
1117 | hist.js |
_dtp / _DTP_DAYS |
1654, 1658 | datepicker.js |
_srvPollTimer |
1777 | opcsvr.js |
t2sMode |
1863 | t2s.js |
fastCurrentSessionId / fastChart / fastLivePollTimer / fastChartTagNames / fastDbConnected |
2640-2644 | fast.js |
pidCurrentPage / pidPageSize / pidLastResult / pidExtracting / pidElapsedInterval / pidPrefixPanelVisible / CATEGORY_META / CATEGORY_ORDER |
3100-3460 | pid.js |
llmSessions / llmActiveSessionId / llmAbortController / llmIsStreaming / llmType / llmUseTools / llmAgentMode / llmMcpTools / LLM_STARTER_CHIPS / llmKbDocMap / _llmSparkSeq / _llmSparkCache / LLM_MAX_HISTORY / LLM_SUMMARY_KEEP |
3670-4262 | llmchat.js |
kbToken / kbExpiresAt / kbCollections / kbPollTimer |
4673-4676 | kbadmin.js |
로드 시 실행되는 최상위 문 (init 훅으로 이전 — D1/D3)
| 문 | 라인 | 조치 |
|---|---|---|
.nav-item 탭 라우터 |
5 | core.js (계획대로) |
certStatus(); |
2593 | cert setup 훅 신설(1회) |
btn-fast-* addEventListener ×7 |
3001-3079 | fast setup 훅(1회, 중복배선 가드) |
[href="#pane-fast"] |
3093 | fast setup 훅 |
btn-pid-export-* ×2 |
3613-3626 | pid setup 훅(1회) |
[data-tab="pid"/"llmchat"/"kbadmin"] |
3640, 3700, 4694 | nav 요소 대상이라 shell에 잔존 → core.js에 통합 가능(pane DOM 비의존) |
정정 로더 (§2.2 · §5.1 대체 — D2/D3 반영)
핵심: ① 진입 시 DOM 주입 완료 후 init, ② 1회 setup(배선) 과 매 진입 enter(데이터 로드) 분리(원동작 보존), ③ 기동 코드로 기본 pane 초기화.
// core.js — 정정판 로더
const paneCache = new Map(); // src → html
const paneReady = new Set(); // pane DOM 주입 완료
const paneSetup = new Set(); // 1회 배선 완료(중복 addEventListener 방지)
// 1회만: pane DOM에 의존하는 최상위 배선 (D1/D3에서 이전)
const paneWire = {
cert: () => certStatus(), // 기존 최상위 호출(2593)
fast: () => fastWire(), // btn-fast-* 배선(3001-3093)을 담은 함수
pid: () => pidWire(), // btn-pid-export-* 배선(3613-3626)
};
// 매 진입: 기존 탭 라우터의 init (데이터 갱신 등)
const paneEnter = {
opcsvr: () => srvLoad(),
t2s: () => t2sInitMode(),
fast: () => fastSessionsLoad(),
docs: () => docsInit(), // docsState.inited 내부 가드 있음
};
async function loadPane(tab) {
const el = document.getElementById(`pane-${tab}`);
if (!el || !el.dataset.src || paneReady.has(tab)) return;
if (!paneCache.has(tab)) {
try { paneCache.set(tab, await (await fetch(el.dataset.src)).text()); }
catch { el.innerHTML = `<div class="card"><p style="color:var(--red)">패널 로딩 실패: ${esc(el.dataset.src)}</p></div>`; return; }
}
el.innerHTML = paneCache.get(tab);
paneReady.add(tab);
}
async function activateTab(tab) {
await loadPane(tab); // ① DOM 주입 완료까지 대기
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
document.getElementById(`pane-${tab}`)?.classList.add('active');
if (!paneSetup.has(tab)) { paneWire[tab]?.(); paneSetup.add(tab); } // ② 1회 배선
paneEnter[tab]?.(); // 매 진입
}
document.querySelectorAll('.nav-item').forEach(item =>
item.addEventListener('click', () => activateTab(item.dataset.tab)));
// ③ 기동(D2): 기본 활성 pane 초기화 — 없으면 첫 화면이 빈 채로 뜸
document.addEventListener('DOMContentLoaded', () => {
const def = document.querySelector('.nav-item.active')?.dataset.tab || 'cert';
activateTab(def);
// Phase 1(eager): 나머지도 미리 주입하려면 ↓ 주석 해제
// document.querySelectorAll('.pane[data-src]').forEach(p => loadPane(p.id.slice(5)));
});
주의:
fastWire/pidWire는 기존 최상위addEventListener블록을 그대로 감싼 함수다.?.옵셔널 체이닝은 유지하되, 반드시 pane DOM 주입 후(=paneWire에서) 호출돼야 배선이 성립한다.
1. 실행 전략
A안(HTML 파셜 fetch) + app.js 탭별 분리 채택.
| 단계 | 내용 | 파일 변경 |
|---|---|---|
| Phase 0 | core.js 생성 + 파셜 로더 |
js/core.js(신규), index.html |
| Phase 1 | pane HTML을 panes/*.html으로 eager 분리 |
panes/*.html(15개 신규), index.html 수정 |
| Phase 2 | app.js를 탭별 JS로 분리 | js/*.js(15개 신규), app.js 삭제 |
| Phase 3 | 지연로딩 전환 | core.js 수정 |
2. Phase 0 — core.js + 파셜 로더
2.1 js/core.js 생성
app.js의 공용 함수를 추출하여 core.js에 배치:
| 함수 | 현재 위치 | 역할 |
|---|---|---|
esc(s) |
app.js:23 | HTML 이스케이프 |
setGlobal(state, text) |
app.js:28 | 전역 상태 표시 |
log(id, lines) |
app.js:33 | 로그박스 출력 |
api(method, path, body) |
app.js:41 | fetch 래퍼 |
fmtTs(v) |
app.js:2599 | 타임스탬프 포맷 |
fmtVal(v) |
app.js:2616 | 값 포맷 |
parseEnumPv(v) |
app.js:2630 | Enum PV 파싱 |
2.2 파셜 로더 구현
⚠️ 아래 초안 로더는 결함(D2 기동 부재 · D3 배선 타이밍) 있음. 실제 구현은 §0.5 "정정 로더"를 사용할 것. 초안은 설계 의도 참고용으로만 남김.
// core.js — 파셜 로더 (초안 · §0.5 정정판으로 대체)
const paneCache = new Map();
const paneInit = {
opcsvr: () => srvLoad(),
t2s: () => t2sInitMode(),
fast: () => fastSessionsLoad(),
docs: () => docsInit(),
};
async function loadPane(tab) {
const el = document.getElementById(`pane-${tab}`);
if (!el || !el.dataset.src) return;
if (!paneCache.has(tab)) {
try {
paneCache.set(tab, await (await fetch(el.dataset.src)).text());
} catch (e) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">패널 로딩 실패: ${esc(el.dataset.src)}</p></div>`;
return;
}
}
el.innerHTML = paneCache.get(tab);
}
async function activateTab(tab) {
await loadPane(tab);
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
const el = document.getElementById(`pane-${tab}`);
if (el) el.classList.add('active');
paneInit[tab]?.();
}
document.querySelectorAll('.nav-item').forEach(item =>
item.addEventListener('click', () => activateTab(item.dataset.tab)));
2.3 index.html 스크립트 로드 순서
<script src="/lib/uPlot.iife.min.js"></script>
<script src="/js/xlsx.full.min.js"></script>
<script src="/js/core.js"></script> <!-- 1. 공용 + 파셜 로더 -->
<script src="/js/cert.js"></script> <!-- 2. 탭별 JS들 -->
<script src="/js/conn.js"></script>
<script src="/js/crawl.js"></script>
<script src="/js/db.js"></script>
<script src="/js/nm-dash.js"></script>
<script src="/js/pb.js"></script>
<script src="/js/hist.js"></script>
<script src="/js/opcsvr.js"></script>
<script src="/js/t2s.js"></script>
<script src="/js/fast.js"></script>
<script src="/js/pid.js"></script>
<script src="/js/evt.js"></script>
<script src="/js/llmchat.js"></script>
<script src="/js/kbadmin.js"></script>
<script src="/js/write.js"></script>
<script src="/js/datepicker.js"></script> <!-- 공용 컴포넌트 -->
<script src="/js/docs.js"></script> <!-- 이미 분리됨 -->
3. Phase 1 — pane HTML eager 분리
3.1 index.html 셸화
1,761줄 → ~120줄로 축소. <main> 내 pane을 data-src 셸로 대체:
<main class="content">
<section class="pane active" id="pane-cert" data-src="/panes/cert.html"></section>
<section class="pane" id="pane-conn" data-src="/panes/conn.html"></section>
<section class="pane" id="pane-crawl" data-src="/panes/crawl.html"></section>
<section class="pane" id="pane-db" data-src="/panes/db.html"></section>
<section class="pane" id="pane-nm-dash" data-src="/panes/nm-dash.html"></section>
<section class="pane" id="pane-pb" data-src="/panes/pb.html"></section>
<section class="pane" id="pane-hist" data-src="/panes/hist.html"></section>
<section class="pane" id="pane-opcsvr" data-src="/panes/opcsvr.html"></section>
<section class="pane" id="pane-t2s" data-src="/panes/t2s.html"></section>
<section class="pane" id="pane-fast" data-src="/panes/fast.html"></section>
<section class="pane" id="pane-pid" data-src="/panes/pid.html"></section>
<section class="pane" id="pane-evt" data-src="/panes/evt.html"></section>
<section class="pane" id="pane-llmchat" data-src="/panes/llmchat.html"></section>
<section class="pane" id="pane-kbadmin" data-src="/panes/kbadmin.html"></section>
<section class="pane" id="pane-write" data-src="/panes/write.html"></section>
<section class="pane" id="pane-docs" data-src="/panes/docs.html"></section>
</main>
모달 2개(fastRecord, datepicker)는 index.html에 유지(pane 외부 공유 컴포넌트).
3.2 panes/*.html 파일 생성
각 pane의 <section> 내용을 그대로 추출. <section> 태그는 제거(로더가 innerHTML로 주입).
| 파일 | 원본 라인 | 내용 |
|---|---|---|
panes/cert.html |
111-151 | 인증서 관리 |
panes/conn.html |
156-208 | 서버 접속 테스트 |
panes/crawl.html |
213-296 | 데이터 크롤링 + 노드맵 수집 |
panes/db.html |
301-352 | DB 저장 |
panes/nm-dash.html |
357-431 | 노드맵 대시보드 |
panes/pb.html |
436-748 | 포인트빌더 |
panes/hist.html |
753-902 | 이력 조회 |
panes/opcsvr.html |
907-933 | OPC UA 서버 |
panes/t2s.html |
938-1028 | Text-to-SQL |
panes/fast.html |
1033-1078 | fastRecord |
panes/pid.html |
1083-1196 | P&ID 추출 |
panes/evt.html |
1201-1280 | 이벤트 히스토리 |
panes/llmchat.html |
1285-1379 | 로컬 LLM 채팅 |
panes/kbadmin.html |
1384-1543 | RAG 관리 |
panes/write.html |
1548-1629 | OPC UA Write |
panes/docs.html |
1634-1672 | 문서 탐색기 |
3.3 Phase 1 완료 시 동작
core.js의 기동 코드가 페이지 로드 시 16개 pane을 모두 eager 주입(§0.5 정정 로더의 DOMContentLoaded에서loadPane일괄 호출 — 초안 §2.2엔 이 루프가 없음, D2)- 단, fetch는 비동기이므로 "DOM 즉시 존재"는 성립하지 않음 → pane DOM에 의존하는 배선/init은 주입 완료 후(
paneWire/paneEnter)에만 실행해야 함(D3) - 동작은 기존과 동일하게 보이되, 파일만 분리됨
4. Phase 2 — app.js 탭별 분리
4.1 분리 매핑표
⚠️ "원본 라인"은 분할표가 아니라 참고용 — 함수가 교차 배치돼 범위가 서로 겹친다(hist⊃evt, t2s⊃core helper). 라인 기준 일괄 추출 금지, 함수명 단위로 이동(§0.5 D5).
| 파일 | 함수 접두어 | 함수 수 | 원본 라인 |
|---|---|---|---|
js/core.js |
esc, setGlobal, log, api, fmtTs, fmtVal, parseEnumPv |
7 | 23-60, 2599-2643 |
js/cert.js |
cert* |
2 | 52-104 |
js/conn.js |
conn*, getServerCfg, llmLoadConfig, llmSaveModelConfig |
5 | 106-261 |
js/crawl.js |
crawl*, nodeMapCrawl |
2 | 263-387 |
js/db.js |
db*, selectFile |
4 | 389-495 |
js/nm-dash.js |
nm* |
5 | 497-614 |
js/pb.js |
pb*, rt*, meta*, subArea* |
31 | 616-1118 |
js/hist.js |
hist*, ht*, renderHistoryTable |
10 | 1120-1658 |
js/opcsvr.js |
srv*, _srv* |
9 | 1779-1866 |
js/t2s.js |
t2s*, toggleMcpMode, loadMcpTools, renderToolsChips, callTool, apiChat*, apiRenderResponse |
28 | 1868-2643 |
js/fast.js |
fast* |
25 | 2646-3102 |
js/pid.js |
pid* |
23 | 3104-3688 |
js/llmchat.js |
llm* |
46 | 3690-4676 |
js/kbadmin.js |
kb* |
32 | 4678-5070 |
js/write.js |
wr* |
4 | 5072-5148 |
js/evt.js |
evt*, _evt* |
8 | 1349-1503 |
js/datepicker.js |
dt*, _dtp |
16 | 1660-1779 |
4.2 분리 규칙
- 함수 단위 이동: 함수 정의 전체를 복사. import 없음 (전역 스코프). ⚠️ prefix grep 기계 분리 금지 —
llmLoadConfig/llmSaveModelConfig(160/177)는llm*지만 conn 소속,certStatus()/core helper는 다른 영역에 끼어있음(§0.5 참조) - 전역 변수:
_t2sLastResult하나가 아님 — 모듈레벨 상태변수 ~25개 전부 이전 필수. §0.5 "모듈레벨 상태변수 전수 인벤토리" 표 기준 - 로드 순서:
core.js먼저, 그다음 탭별 JS.datepicker가 hist/evt보다 먼저→ 불필요(전부function선언 호이스팅, 순서 무관). 단 로드 시 실행되는 최상위 문은 §0.5대로 init 훅으로 이전해야 함(D4) - docs.js: 이미 분리됨. 변경 없음
4.3 app.js 삭제
모든 함수가 분리된 후 app.js 삭제. app.js.backup는 유지(롤백용).
5. Phase 3 — 지연로딩 전환
5.1 eager → lazy 변경
core.js의 loadPane()을 수정하여 탭 진입 시에만 fetch. ⚠️ §0.5 정정 로더는 이미 lazy(진입 시 loadPane) + 기동 코드(기본 pane) 포함 — 별도 전환 불필요. 아래 초안은 기동·배선 누락(D2/D3) 상태이므로 §0.5판을 사용:
const paneLoaded = new Set();
async function loadPane(tab) {
if (paneLoaded.has(tab)) return;
const el = document.getElementById(`pane-${tab}`);
if (!el || !el.dataset.src) return;
try {
el.innerHTML = await (await fetch(el.dataset.src)).text();
paneLoaded.add(tab);
} catch (e) {
el.innerHTML = `<div class="card"><p style="color:var(--red)">패널 로딩 실패</p></div>`;
}
}
5.2 init 타이밍 검증
지연로딩 전환 시 반드시 검증할 항목:
| 탭 | init 함수 | 검증 사항 |
|---|---|---|
| opcsvr | srvLoad() |
srvLoad()가 pane 내부 DOM을 참조하는지 확인 |
| t2s | t2sInitMode() |
同上 |
| fast | fastSessionsLoad() |
同上 |
| docs | docsInit() |
docsState.inited 플래그로 안전 |
리스크: index.html에 인라인되어 있던 onclick="..."은 innerHTML 파싱 시 정상 바인딩됨. <script> 태그는 innerHTML로 실행되지 않으므로 pane 파일에는 <script> 포함 금지.
🔴 추가 리스크(D3): pane 내부 버튼을
onclick인라인이 아니라 최상위addEventListener로 배선하는 탭이 있음 — fast(btn-fast-*3001-3093), pid(btn-pid-export-*3613-3626). 이 배선은 스크립트 로드 시점에 실행되는데, 파셜 pane은 그 시점에 DOM에 없어?.로 조용히 무력화된다(eager·lazy 공통). 반드시 §0.5 정정 로더의paneWire(1회 setup 훅) 안에서, pane DOM 주입 후 배선할 것.[data-tab="..."]/.nav-item대상 배선은 shell에 남는 nav 요소라 안전.
6. 실행 순서 및 검증
6.1 Phase별 실행 순서
Phase 0: core.js 생성 + 파셜 로더
↓ 검증: 빌드 성공, 탭 전환 동작
Phase 1: panes/*.html eager 분리
↓ 검증: 모든 탭 동작 동일, 네트워크 탭에서 16개 fetch 확인(panes/*.html)
Phase 2: app.js 탭별 분리
↓ 검증: 모든 탭 동작 동일, app.js 삭제
Phase 3: 지연로딩 전환
↓ 검증: 탭 진입 시 fetch, init 타이밍 정상
6.2 검증 명령
# 빌드
dotnet build src/Web/ExperionCrawler.csproj
# 테스트
dotnet test
6.3 롤백 전략
| Phase | 롤백 방법 |
|---|---|
| 0 | core.js 삭제, index.html 스크립트 태그 복원 |
| 1 | panes/*.html 내용을 index.html에 복붙, data-src 제거 |
| 2 | app.js.backup → app.js 복원, 탭별 JS 삭제 |
| 3 | core.js의 paneLoaded 제거, eager 주입으로 복귀 |
7. 체크리스트 교차 검증 (STEP 6)
Q1. 이미 수정된 문제인가?
docs.js는 이미 분리되어 있음 → 중복 작업 아님 ✅app.js의 탭 라우터는app.js:5-20에 존재 → 아직 분리되지 않음 ✅
Q2. 다른 레이어에서 처리되고 있는가?
UseStaticFiles()는panes/*.html을 그대로 서빙 → 추가 설정 불필요 ✅MapFallbackToFile("index.html")은 SPA 라우팅용 → panes fetch와 충돌 없음 ✅
Q3. 의도적 설계인가?
- "빌드 스텝 없음"은 AGENTS.md에 명시된 의도적 설계 → 준수 ✅
docs.js의 분리 패턴은 이미 입증된 모범 사례 ✅
Q4. 실제 장애 시나리오가 있는가?
- 1,761줄 HTML 파일에서 한 탭 수정 시 전체 파일 열어야 함 → 편집 충돌 발생 가능 ✅
- 5,148줄 JS에서 함수 검색이 어려움 → 디버깅 시간 증가 ✅
8. CSS 분리 (Phase 4, 선택)
우선순위 낮음. HTML/JS 분리 완료 후 진행.
| 파일 | 분리 대상 |
|---|---|
css/style.css (2,231줄) |
공용 토큰/레이아웃만 유지 |
css/llmchat.css (신규) |
.llm-* 클래스 추출 |
css/kbadmin.css (신규) |
.kb-* 클래스 추출 |
css/pb.css (신규) |
.pb-* 클래스 추출 |
9. 자가 검증 (STEP 8)
- 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킬 수 있는가? →
app.js:5-20,index.html:111-1672등 - HIGH 항목은 재현 가능한 시나리오를 한 문장으로 말할 수 있는가? → "한 탭 수정 시 1,761줄 파일 전체를 열어야 해서 두 사람이 동시에 편집 시 충돌 발생"
- 교차 검증 4개 질문을 모두 통과한 항목만 포함되어 있는가? → 위 §7 참조
- 보고서의 수정 예시가 현재 코드에 아직 적용되지 않은 내용인가? →
core.js/panes/*.html은 아직 존재하지 않음 - "더 좋은 방법 제안"과 "현재 코드가 틀렸다"를 혼동하지 않았는가? → 현재 코드는 정상 동작. 유지보수성 개선 제안임