Files
C.E.L_Slide_test2/tests/matching/WEEKLY_REPORT_DASHBOARD.html
T
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:03:42 +09:00

966 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CEL 슬라이드 변환 주간 보고</title>
<script src="./weekly_report_data.js"></script>
<style>
:root {
--bg: #eef4ff;
--panel: rgba(255, 255, 255, 0.92);
--panel-strong: #ffffff;
--line: #d8e4ff;
--line-strong: #9bb9ff;
--text: #12233f;
--muted: #6f7f99;
--blue: #2f74ff;
--blue-dark: #1a57cf;
--green: #0f9f5a;
--red: #d64747;
--amber: #d88f16;
--shadow: 0 18px 48px rgba(34, 73, 150, 0.12);
--radius: 18px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", Pretendard, "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
color: var(--text);
background:
linear-gradient(180deg, rgba(225,240,255,0.9) 0%, rgba(239,245,255,0.98) 180px, #f5f8ff 360px),
radial-gradient(circle at top right, rgba(72, 174, 255, 0.18), transparent 32%),
linear-gradient(180deg, #a6ddff 0%, #d6efff 14%, #f4f8ff 30%, #f5f8ff 100%);
}
.app {
max-width: 1560px;
margin: 0 auto;
padding: 22px 22px 28px;
}
.topbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 16px;
padding: 4px 4px 10px;
border-bottom: 1px solid rgba(47, 116, 255, 0.12);
}
.headline {
display: flex;
flex-direction: column;
gap: 4px;
}
.headline h1 {
margin: 0;
font-size: 2.6rem;
line-height: 1.05;
letter-spacing: -0.03em;
color: #17315f;
}
.headline p {
margin: 0;
font-size: 0.96rem;
color: var(--muted);
}
.updated {
font-size: 0.9rem;
color: var(--muted);
white-space: nowrap;
padding-top: 8px;
}
.layout {
display: grid;
grid-template-columns: 170px minmax(0, 1fr) 360px;
gap: 18px;
align-items: start;
}
.panel {
background: var(--panel);
border: 1px solid rgba(255,255,255,0.7);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.sidebar {
padding: 16px;
position: sticky;
top: 18px;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
font-size: 1.02rem;
font-weight: 700;
color: #17315f;
}
.week-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 18px;
}
.week-button {
border: 1px solid var(--line);
background: rgba(255,255,255,0.75);
border-radius: 14px;
padding: 12px 10px;
text-align: center;
cursor: pointer;
transition: 0.18s ease;
color: var(--text);
}
.week-button:hover {
border-color: var(--line-strong);
transform: translateY(-1px);
}
.week-button.active {
background: linear-gradient(180deg, #2f74ff 0%, #215fd8 100%);
color: white;
border-color: transparent;
box-shadow: 0 10px 20px rgba(47, 116, 255, 0.28);
}
.week-main {
font-size: 1.05rem;
font-weight: 700;
margin-bottom: 2px;
}
.week-range {
font-size: 0.86rem;
opacity: 0.84;
}
.tip-card {
margin-top: 16px;
padding: 14px 12px;
border-radius: 14px;
background: #f3f8ff;
border: 1px solid #d9e8ff;
color: #527099;
font-size: 0.88rem;
line-height: 1.55;
}
.main {
display: grid;
gap: 16px;
}
.section {
padding: 18px 20px;
}
.process-wrap {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding-top: 8px;
position: relative;
}
.process-wrap::before {
content: "";
position: absolute;
top: 26px;
left: 54px;
right: 54px;
height: 3px;
background: linear-gradient(90deg, #2f74ff 0%, #c9d8fb 55%, #d8dfef 100%);
z-index: 0;
}
.step {
position: relative;
z-index: 1;
flex: 1;
text-align: center;
min-width: 0;
}
.step-badge {
width: 38px;
height: 38px;
margin: 0 auto 10px;
border-radius: 50%;
border: 3px solid #c7d6f8;
background: white;
display: grid;
place-items: center;
font-weight: 800;
color: #72819d;
box-shadow: 0 6px 12px rgba(28, 82, 186, 0.06);
}
.step.done .step-badge,
.step.current .step-badge {
border-color: var(--blue);
color: white;
background: linear-gradient(180deg, #3b81ff 0%, #2569e1 100%);
}
.step-title {
font-size: 0.97rem;
font-weight: 800;
color: #17315f;
margin-bottom: 3px;
}
.step-subtitle {
font-size: 0.84rem;
color: var(--muted);
}
.summary-list {
display: grid;
gap: 10px;
margin-top: 8px;
}
.summary-item {
border: 1px solid #e2ebff;
border-radius: 14px;
background: rgba(255,255,255,0.82);
overflow: hidden;
}
.summary-head {
display: grid;
grid-template-columns: 28px 1fr auto 18px;
gap: 12px;
align-items: center;
width: 100%;
padding: 13px 14px;
background: transparent;
border: 0;
text-align: left;
cursor: pointer;
color: inherit;
}
.summary-index {
width: 28px;
height: 28px;
border-radius: 50%;
display: grid;
place-items: center;
background: #edf4ff;
color: var(--blue-dark);
font-weight: 800;
font-size: 0.84rem;
}
.summary-title {
font-size: 1rem;
font-weight: 700;
}
.summary-files {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.file-chip {
padding: 4px 8px;
border-radius: 999px;
background: #f1f6ff;
border: 1px solid #dbe8ff;
color: #4b67a0;
font-size: 0.77rem;
}
.summary-arrow {
color: #7c8ca8;
transition: transform 0.18s ease;
}
.summary-item.open .summary-arrow {
transform: rotate(180deg);
}
.summary-body {
display: none;
padding: 0 14px 14px 54px;
color: #506177;
font-size: 0.93rem;
line-height: 1.6;
}
.summary-item.open .summary-body {
display: block;
}
.daily-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
margin-top: 6px;
}
.day-card {
border: 1px solid #e3ecff;
border-radius: 16px;
background: rgba(255,255,255,0.82);
padding: 14px;
min-height: 182px;
display: flex;
flex-direction: column;
gap: 10px;
}
.day-date {
font-size: 1rem;
font-weight: 800;
color: var(--blue-dark);
}
.day-bullets {
margin: 0;
padding-left: 18px;
color: #3f4d64;
font-size: 0.9rem;
line-height: 1.5;
display: grid;
gap: 4px;
}
.day-meta {
margin-top: auto;
font-size: 0.78rem;
color: var(--muted);
border-top: 1px dashed #d8e4ff;
padding-top: 8px;
}
.issue-plan {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.subpanel {
border: 1px solid #e2ebff;
border-radius: 16px;
background: rgba(255,255,255,0.8);
padding: 14px 16px;
}
.subpanel h3 {
margin: 0 0 12px;
font-size: 1rem;
color: #17315f;
}
.dot-list {
margin: 0;
padding-left: 0;
list-style: none;
display: grid;
gap: 10px;
}
.dot-item {
display: grid;
grid-template-columns: 20px 1fr;
gap: 10px;
font-size: 0.92rem;
line-height: 1.55;
color: #3d4e65;
}
.dot-issue { color: var(--red); }
.dot-plan { color: var(--green); }
.dot-decision { color: var(--amber); }
.right {
display: grid;
gap: 16px;
position: sticky;
top: 18px;
}
.attachments {
padding: 16px;
}
.attach-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.attach-tab {
padding: 6px 10px;
border-radius: 999px;
background: #f2f6ff;
border: 1px solid #dce8ff;
color: #55709e;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
}
.attach-tab.active {
background: #edf4ff;
border-color: #a9c4ff;
color: var(--blue-dark);
}
.attach-list {
display: grid;
gap: 8px;
max-height: 300px;
overflow: auto;
padding-right: 4px;
}
.attach-item {
border: 1px solid #e0e8fb;
border-radius: 14px;
background: rgba(255,255,255,0.84);
padding: 11px 12px;
cursor: pointer;
transition: 0.16s ease;
}
.attach-item:hover,
.attach-item.active {
border-color: #afc7ff;
box-shadow: 0 8px 18px rgba(47,116,255,0.1);
transform: translateY(-1px);
}
.attach-name {
font-size: 0.92rem;
font-weight: 700;
color: #17315f;
margin-bottom: 2px;
word-break: break-all;
}
.attach-meta {
font-size: 0.79rem;
color: var(--muted);
}
.preview {
margin-top: 14px;
border-top: 1px solid #e1ebff;
padding-top: 14px;
display: grid;
gap: 10px;
}
.preview-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.preview-title {
font-size: 0.97rem;
font-weight: 800;
color: #17315f;
word-break: break-all;
}
.preview-link {
color: var(--blue-dark);
text-decoration: none;
font-size: 0.83rem;
font-weight: 700;
white-space: nowrap;
}
.preview-box {
min-height: 360px;
border: 1px solid #dfe9ff;
background: #fbfdff;
border-radius: 16px;
overflow: hidden;
display: grid;
place-items: center;
}
.preview-frame,
.preview-image {
width: 100%;
height: 360px;
border: 0;
background: white;
}
.preview-text {
padding: 22px;
width: 100%;
height: 100%;
color: #4f6077;
line-height: 1.7;
font-size: 0.92rem;
}
.mini-panel {
padding: 14px 16px;
}
.mini-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
font-size: 0.92rem;
padding: 10px 0;
border-top: 1px solid #edf2ff;
}
.mini-row:first-of-type { border-top: 0; }
.mini-count {
min-width: 28px;
padding: 2px 9px;
border-radius: 999px;
background: #eef4ff;
color: var(--blue-dark);
text-align: center;
font-weight: 800;
font-size: 0.8rem;
}
.decision-list {
margin-top: 14px;
border-top: 1px solid #e4edff;
padding-top: 14px;
}
.empty {
color: var(--muted);
font-size: 0.9rem;
line-height: 1.6;
}
@media (max-width: 1320px) {
.layout {
grid-template-columns: 160px minmax(0, 1fr);
}
.right {
grid-column: 1 / -1;
position: static;
}
}
@media (max-width: 1024px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
.daily-grid,
.issue-plan {
grid-template-columns: 1fr;
}
.process-wrap {
overflow-x: auto;
padding-bottom: 8px;
}
.process-wrap::before {
left: 40px;
right: 40px;
}
.topbar {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="app">
<header class="topbar">
<div class="headline">
<h1 id="project-title">CEL 슬라이드 변환 주간 보고</h1>
<p id="project-subtitle">Git 기반 자동 보고 시스템</p>
</div>
<div class="updated" id="last-updated">Last updated 2026.04.24 17:30</div>
</header>
<div class="layout">
<aside class="sidebar panel">
<div class="panel-title">주차 선택</div>
<div id="week-list" class="week-list"></div>
<div class="tip-card">
매일은 Git/Gitea 기록을 일단위로 축적하고, 목요일에는 해당 기간을 주간 단위로 다시 요약하는 구조를 상정했습니다.
</div>
</aside>
<main class="main">
<section class="panel section">
<div class="panel-title">전체 수행 프로세스</div>
<div id="process-wrap" class="process-wrap"></div>
</section>
<section class="panel section">
<div class="panel-title">주간 수행 현황</div>
<div id="summary-list" class="summary-list"></div>
</section>
<section class="panel section">
<div class="panel-title">일자별 수행 내역 (요약)</div>
<div id="daily-grid" class="daily-grid"></div>
</section>
<section class="panel section">
<div class="panel-title">이슈 및 향후 계획</div>
<div class="issue-plan">
<div class="subpanel">
<h3>주요 이슈</h3>
<ul id="issue-list" class="dot-list"></ul>
</div>
<div class="subpanel">
<h3>향후 계획</h3>
<ul id="plan-list" class="dot-list"></ul>
</div>
</div>
<div class="decision-list">
<div class="subpanel">
<h3>결정 사항</h3>
<ul id="decision-list" class="dot-list"></ul>
</div>
</div>
</section>
</main>
<aside class="right">
<section class="panel attachments">
<div class="panel-title">첨부파일 view</div>
<div id="attach-tabs" class="attach-tabs"></div>
<div id="attach-list" class="attach-list"></div>
<div class="preview">
<div class="preview-head">
<div id="preview-title" class="preview-title">첨부를 선택하세요</div>
<a id="preview-link" class="preview-link" href="#" target="_blank" rel="noopener">새 탭 열기</a>
</div>
<div id="preview-box" class="preview-box">
<div class="preview-text">오른쪽 첨부 목록에서 파일을 선택하면 이곳에 미리보기가 표시됩니다.</div>
</div>
</div>
</section>
<section class="panel mini-panel">
<div class="panel-title">파일 변경 히스토리</div>
<div class="mini-row">
<span>이번 주 파일 변경 수</span>
<span id="history-count" class="mini-count">0</span>
</div>
<div class="mini-row">
<span>관련 이슈 수</span>
<span id="issue-count" class="mini-count">0</span>
</div>
<div class="mini-row">
<span>브랜치 / PR 현황</span>
<span id="pr-count" class="mini-count">0</span>
</div>
</section>
<section class="panel mini-panel">
<div class="panel-title">안내</div>
<div class="empty" id="meta-hint">
현재는 샘플 데이터 기반 구조입니다. 다음 단계에서는 git/Gitea/GitHub 로그와 attachment 매핑을 연결해 자동 갱신 구조로 확장할 수 있습니다.
</div>
</section>
</aside>
</div>
</div>
<script>
(function () {
const data = window.WEEKLY_REPORT_DATA;
if (!data || !Array.isArray(data.weeks) || !data.weeks.length) {
document.body.innerHTML = "<div style='padding:24px;font-family:sans-serif'>주간 보고 데이터가 없습니다.</div>";
return;
}
const state = {
weekId: data.weeks.find((w) => w.status === "active")?.id || data.weeks[0].id,
filter: "전체",
selectedAttachmentName: null,
};
const el = {
projectTitle: document.getElementById("project-title"),
projectSubtitle: document.getElementById("project-subtitle"),
lastUpdated: document.getElementById("last-updated"),
weekList: document.getElementById("week-list"),
processWrap: document.getElementById("process-wrap"),
summaryList: document.getElementById("summary-list"),
dailyGrid: document.getElementById("daily-grid"),
issueList: document.getElementById("issue-list"),
planList: document.getElementById("plan-list"),
decisionList: document.getElementById("decision-list"),
attachTabs: document.getElementById("attach-tabs"),
attachList: document.getElementById("attach-list"),
previewTitle: document.getElementById("preview-title"),
previewLink: document.getElementById("preview-link"),
previewBox: document.getElementById("preview-box"),
historyCount: document.getElementById("history-count"),
issueCount: document.getElementById("issue-count"),
prCount: document.getElementById("pr-count"),
metaHint: document.getElementById("meta-hint"),
};
el.projectTitle.textContent = data.meta.project_title;
el.projectSubtitle.textContent = data.meta.subtitle;
el.lastUpdated.textContent = "Last updated " + data.meta.last_updated;
el.metaHint.textContent = data.meta.source_hint;
function getCurrentWeek() {
return data.weeks.find((w) => w.id === state.weekId) || data.weeks[0];
}
function countByCategory(attachments) {
const map = { "전체": attachments.length };
attachments.forEach((item) => {
const key = item.category || "기타";
map[key] = (map[key] || 0) + 1;
});
return map;
}
function iconForCategory(category) {
switch (category) {
case "문서": return "문서";
case "이미지": return "이미지";
case "코드": return "코드";
default: return "기타";
}
}
function renderWeeks() {
el.weekList.innerHTML = "";
data.weeks.forEach((week) => {
const button = document.createElement("button");
button.className = "week-button" + (week.id === state.weekId ? " active" : "");
button.type = "button";
button.innerHTML = `
<div class="week-main">${week.label}</div>
<div class="week-range">(${week.range})</div>
`;
button.addEventListener("click", () => {
state.weekId = week.id;
state.filter = "전체";
state.selectedAttachmentName = null;
render();
});
el.weekList.appendChild(button);
});
}
function renderProcess(week) {
el.processWrap.innerHTML = "";
week.process_steps.forEach((step) => {
const node = document.createElement("div");
node.className = "step " + step.state;
node.innerHTML = `
<div class="step-badge">${step.id}</div>
<div class="step-title">${step.title}</div>
<div class="step-subtitle">${step.subtitle}</div>
`;
el.processWrap.appendChild(node);
});
}
function renderSummary(week) {
el.summaryList.innerHTML = "";
week.weekly_summary.forEach((item, idx) => {
const box = document.createElement("div");
box.className = "summary-item" + (idx === 0 ? " open" : "");
const filesHtml = (item.files || []).map((file) => `<span class="file-chip">${file}</span>`).join("");
box.innerHTML = `
<button class="summary-head" type="button">
<div class="summary-index">${idx + 1}</div>
<div class="summary-title">${item.title}</div>
<div class="summary-files">${filesHtml}</div>
<div class="summary-arrow">⌄</div>
</button>
<div class="summary-body">${item.detail}</div>
`;
box.querySelector(".summary-head").addEventListener("click", () => {
box.classList.toggle("open");
});
el.summaryList.appendChild(box);
});
}
function renderDaily(week) {
el.dailyGrid.innerHTML = "";
week.daily_logs.forEach((day) => {
const node = document.createElement("div");
node.className = "day-card";
node.innerHTML = `
<div class="day-date">${day.date} (${day.weekday})</div>
<ul class="day-bullets">
${day.bullets.map((bullet) => `<li>${bullet}</li>`).join("")}
</ul>
<div class="day-meta">관련 파일 ${day.file_count}개</div>
`;
el.dailyGrid.appendChild(node);
});
}
function renderTextList(container, items, kind) {
container.innerHTML = "";
if (!items || !items.length) {
container.innerHTML = `<li class="empty">등록된 내용이 없습니다.</li>`;
return;
}
items.forEach((item) => {
const li = document.createElement("li");
li.className = "dot-item";
const iconClass = kind === "issue" ? "dot-issue" : kind === "plan" ? "dot-plan" : "dot-decision";
const icon = kind === "issue" ? "•" : kind === "plan" ? "✓" : "◦";
li.innerHTML = `<span class="${iconClass}">${icon}</span><span>${item}</span>`;
container.appendChild(li);
});
}
function renderAttachmentTabs(week) {
el.attachTabs.innerHTML = "";
const counts = countByCategory(week.attachments);
Object.entries(counts).forEach(([label, count]) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "attach-tab" + (state.filter === label ? " active" : "");
btn.textContent = `${label} ${count}`;
btn.addEventListener("click", () => {
state.filter = label;
state.selectedAttachmentName = null;
renderAttachments(week);
});
el.attachTabs.appendChild(btn);
});
}
function filteredAttachments(week) {
if (state.filter === "전체") return week.attachments;
return week.attachments.filter((item) => (item.category || "기타") === state.filter);
}
function renderAttachments(week) {
const items = filteredAttachments(week);
el.attachList.innerHTML = "";
const selected =
items.find((item) => item.name === state.selectedAttachmentName) ||
items.find((item) => item.name === week.preview_target) ||
items[0] ||
null;
if (selected) {
state.selectedAttachmentName = selected.name;
}
items.forEach((item) => {
const node = document.createElement("button");
node.type = "button";
node.className = "attach-item" + (selected && selected.name === item.name ? " active" : "");
node.innerHTML = `
<div class="attach-name">${item.name}</div>
<div class="attach-meta">${iconForCategory(item.category)} · ${item.date} · ${item.size}</div>
`;
node.addEventListener("click", () => {
state.selectedAttachmentName = item.name;
renderAttachments(week);
});
el.attachList.appendChild(node);
});
renderPreview(selected);
}
function renderPreview(item) {
if (!item) {
el.previewTitle.textContent = "첨부가 없습니다";
el.previewLink.href = "#";
el.previewBox.innerHTML = `<div class="preview-text">표시할 첨부파일이 없습니다.</div>`;
return;
}
el.previewTitle.textContent = item.name;
el.previewLink.href = item.url;
const ext = item.name.split(".").pop().toLowerCase();
if (ext === "html" || ext === "pdf") {
el.previewBox.innerHTML = `<iframe class="preview-frame" src="${item.url}" title="${item.name}"></iframe>`;
} else if (["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext)) {
el.previewBox.innerHTML = `<img class="preview-image" src="${item.url}" alt="${item.name}">`;
} else {
el.previewBox.innerHTML = `
<div class="preview-text">
<strong>${item.name}</strong><br><br>
${item.description || "미리보기 대신 메타데이터만 제공합니다."}<br><br>
유형: ${item.type}<br>
분류: ${item.category}<br>
크기: ${item.size}<br>
날짜: ${item.date}<br><br>
새 탭 열기를 눌러 원본 파일을 확인할 수 있습니다.
</div>
`;
}
}
function renderMeta(week) {
el.historyCount.textContent = week.file_history_count;
el.issueCount.textContent = week.issue_count;
el.prCount.textContent = week.pr_count;
}
function render() {
const week = getCurrentWeek();
renderWeeks();
renderProcess(week);
renderSummary(week);
renderDaily(week);
renderTextList(el.issueList, week.issues, "issue");
renderTextList(el.planList, week.plans, "plan");
renderTextList(el.decisionList, week.decisions || [], "decision");
renderAttachmentTabs(week);
renderAttachments(week);
renderMeta(week);
}
render();
})();
</script>
</body>
</html>