단일 파일이 프로젝트 상한(800줄)의 6.8배였다. 분할은 순수 라인범위 추출이며 어떤 구문도 수정·재배치·재포맷하지 않았다 — 15개 파트의 연결 결과가 원본 app.js 와 바이트 단위로 동일함을 확인하고 나눴다(125,689B). 클래식 스크립트(모듈 아님)라 하나의 전역 스코프를 공유하므로 로드 순서가 곧 계약이다. 원본에서 로드 시점에 실행되는 최상위 구문은 파일 끝의 리스너 등록 2개뿐이었고(DOMContentLoaded/hashchange), 나머지 참조는 전부 함수 본문 안이라 순서만 지키면 의미가 보존된다. 그 순서를 index.html 에 명시하고 테스트로 고정: - test_index_loads_every_app_part_exactly_once_in_declared_order 각 파트가 정확히 1회, 선언 순서대로 로드되는지 + 디스크의 app-*.js 가 전부 배선됐는지 + 구 app.js 가 남아있지 않은지 - test_app_parts_stay_within_the_file_size_budget (800줄 상한) 정적 스펙 31곳은 app.js 본문을 문자열로 검사하고 있었다. 이 스펙들의 관심사는 '어느 파일에 있는가'가 아니라 '콘솔에 그 동작이 있는가'이므로, 파트를 로드 순서대로 이어붙여 읽는 _read_app() 으로 바꿨다. 검증: Playwright E2E 6건이 실브라우저에서 통과(콘솔 에러 0 단언 포함). 운영 콘솔 실렌더 확인 — 큐/케이스 심사 정상, 스크립트 15개 전부 200, 새로고침 후 콘솔 메시지 0건.
137 lines
2.5 KiB
JavaScript
137 lines
2.5 KiB
JavaScript
// Part 1 of 15 of the operator console app script.
|
|
//
|
|
// Label imports and the in-memory client state every other part reads.
|
|
//
|
|
// Load order is significant: these are classic (non-module) scripts sharing one
|
|
// global scope, and index.html must keep them in the order listed above/below.
|
|
// Splitting is a pure line-range extraction of the former single app.js.
|
|
|
|
const {
|
|
riskLabels,
|
|
decisionLabels,
|
|
providerLabels,
|
|
sourceLabels,
|
|
providerStatusLabels,
|
|
candidateStatusLabels,
|
|
evidenceStatusLabels,
|
|
knowledgeStatusLabels,
|
|
provenanceLabels,
|
|
knowledgeTypeLabels,
|
|
} = window.OperatorLabels;
|
|
|
|
const retiredProviderIds = new Set();
|
|
const operatorSearchProviders = [
|
|
{ id: "naver", label: providerLabels.naver },
|
|
{ id: "google_search", label: providerLabels.google_search },
|
|
];
|
|
const submissions = [];
|
|
|
|
|
|
|
|
const providers = [];
|
|
|
|
|
|
|
|
const knowledgeEntries = [];
|
|
|
|
|
|
|
|
const searchCoverage = {};
|
|
|
|
let suggestedQueryRunSummary = null; // { caseId, message } — 현재 케이스의 마지막 추천 쿼리 실행 결과
|
|
|
|
const knowledgeFilters = { query: "", type: "all", status: "all", active: "all" };
|
|
let editingKnowledgeEntryId = "";
|
|
|
|
let activeRerunAddedIds = new Set(); // 현재 렌더 중인 케이스의 lastRerunDiff.addedEvidenceIds
|
|
|
|
const DEFAULT_COVERAGE_THRESHOLDS = Object.freeze({
|
|
|
|
coverageGoodRate: 70,
|
|
|
|
coverageWarnRate: 40,
|
|
|
|
queryGoodRate: 70,
|
|
|
|
queryWarnRate: 40,
|
|
|
|
});
|
|
|
|
const coverageThresholds = { ...DEFAULT_COVERAGE_THRESHOLDS };
|
|
|
|
|
|
|
|
const collectionCandidates = [];
|
|
|
|
|
|
|
|
const corrections = [];
|
|
|
|
|
|
|
|
const auditEvents = [];
|
|
|
|
|
|
|
|
const decisionMemoPresets = [];
|
|
|
|
|
|
|
|
const state = {
|
|
|
|
currentView: "queue",
|
|
|
|
workbenchTab: "evidence",
|
|
|
|
knowledgeTab: "collect",
|
|
|
|
currentCollectionQuery: "",
|
|
|
|
currentCollectionProvider: "",
|
|
|
|
selectedCaseId: submissions[0]?.id || "",
|
|
|
|
apiError: "",
|
|
|
|
submissionQueue: null,
|
|
|
|
importStatus: {
|
|
|
|
tone: "idle",
|
|
|
|
message: "로컬 제출 저장소 변경 대기 중",
|
|
|
|
},
|
|
|
|
filters: {
|
|
|
|
risk: "all",
|
|
|
|
source: "all",
|
|
|
|
decision: "all",
|
|
|
|
sort: "risk",
|
|
|
|
query: "",
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const viewNames = new Set(["queue", "workbench", "knowledge", "providers", "stats", "audit"]);
|
|
|
|
|
|
|
|
// Bearer token for a server started with COPYRIGHTER_AUTH_TOKEN. Held in memory
|
|
// only (never localStorage/sessionStorage): the first authenticated request makes
|
|
// the server mint an HttpOnly session cookie, which is what subsequently
|
|
// authorizes <img>/media requests that cannot carry a header.
|
|
|
|
let authToken = "";
|
|
|
|
let pendingAuthPrompt = null;
|
|
|
|
|