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>
This commit is contained in:
@@ -0,0 +1,965 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user