IMPROVEMENT Phase A~D + Phase 2 전체 반영
## IMPROVEMENT (Phase A~D) - A-1: 4단계 Sonnet 디자인 조정 (_adjust_design) — CSS 변수 cascade - A-2: 5단계 HTML 전문 프롬프트 전달 - A-3: shrink/expand 하드코딩 제거 → Sonnet target_ratio 기반 - A-4: rewrite action 구현 - A-5: overflow: visible (area 레벨 텍스트 잘림 방지) - A-6: object-fit cover → contain (이미지 crop 방지) - A-7: table-layout: fixed - A-8: container query 폰트 스케일링 - B-1: details-block 템플릿 신규 (CSS 변수만 사용) - B-2: 인쇄 시 details 자동 펼침 JS - B-3: catalog에 details-block 등록 - B-4/B-5: images[]/tables[] 상세 판단 + fallback 3곳 동기화 - B-8: fallback card-grid → topic-header + char_guide 제거 - C-1: CLAUDE.md gradient 원칙 완화 - C-3: border-radius 9개 파일 var(--radius) 통일 - C-4: box-shadow 2레벨 → 1레벨 - D-0: 이미지 경로 입력 UI + API base_path - D-1: Pillow 의존성 + image_utils.py - D-2~D-4: 이미지 비율/축소방지 프롬프트 전달 - D-5: HTML에 이미지 base64 삽입 ## Phase 2 (다른 Claude 작업) - P2-A: FAISS 블록 검색 (bge-m3, 46개 블록) - P2-B: SVG N개 자동 배치 (svg_calculator.py) - P2-C: Opus 블록 추천 (Kei API 경유) - P2-D: 5단계 재검토 루프 강화 (MAX_REVIEW_ROUNDS=2) - P2-E: details-block fallback 연동 ## 버그 수정 (BF-8~10) - BF-8: 컨테이너 예산 기반 블록 배치 - BF-9: grid와 Sonnet 역할 분리 - BF-10: catalog mtime 캐시 자동 갱신 ## 블록 라이브러리 - 46개 블록 (6 카테고리), catalog/BLOCK_SLOTS/INDEX 동기화 - 구 블록 제거 (quote-block, card-grid, comparison) - 13개 _legacy 블록 보존 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
templates/blocks/visuals/circle-gradient.html
Normal file
58
templates/blocks/visuals/circle-gradient.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!-- 원형 라벨: CSS 그라데이션 원 + 중앙 텍스트 -->
|
||||
<!--
|
||||
📋 circle-label
|
||||
─────────────────
|
||||
용도: 섹션 전환점, 핵심 키워드 강조, 시각적 구분자
|
||||
슬롯: label (필수), sub_label (선택)
|
||||
Figma 원본: 2-1_02 > Group 1171281590 (190x190)
|
||||
-->
|
||||
<div class="block-circle-label">
|
||||
<div class="cl-outer">
|
||||
<div class="cl-inner">
|
||||
<div class="cl-text">{{ label }}</div>
|
||||
{% if sub_label %}<div class="cl-sub">{{ sub_label }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-circle-label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
min-height: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.cl-outer {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #3db8ff 0%, #006aff 100%);
|
||||
box-shadow: 0 0 30px rgba(0, 106, 255, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cl-inner {
|
||||
width: 170px;
|
||||
height: 170px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #4dc4ff 0%, #0080ff 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
.cl-text {
|
||||
font-size: 20px;
|
||||
font-weight: var(--weight-bold, 700);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cl-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
74
templates/blocks/visuals/compare-pill-pair.html
Normal file
74
templates/blocks/visuals/compare-pill-pair.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!-- 비교 박스: 이중 테두리 둥근 박스 2개 + VS -->
|
||||
<!--
|
||||
📋 compare-pill-pair
|
||||
─────────────────
|
||||
용도: 2개 개념 시각적 대비 (비교 테이블 위 헤더)
|
||||
슬롯: left_label, left_sub, right_label, right_sub
|
||||
Figma 원본: 이중 테두리 (바깥 얇은 선 + 안쪽 둥근 박스)
|
||||
-->
|
||||
<div class="block-compare-pill">
|
||||
<div class="cp-item">
|
||||
<div class="cp-outer">
|
||||
<div class="cp-inner">
|
||||
<div class="cp-label">{{ left_label }}</div>
|
||||
{% if left_sub %}<div class="cp-sub">{{ left_sub }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cp-vs">VS</div>
|
||||
<div class="cp-item">
|
||||
<div class="cp-outer">
|
||||
<div class="cp-inner">
|
||||
<div class="cp-label">{{ right_label }}</div>
|
||||
{% if right_sub %}<div class="cp-sub">{{ right_sub }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-compare-pill {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.cp-item {
|
||||
width: 350px;
|
||||
}
|
||||
.cp-outer {
|
||||
border: 2px solid #a8d8f0;
|
||||
border-radius: 60px;
|
||||
padding: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
.cp-inner {
|
||||
border: 2px solid #7ec8f0;
|
||||
border-radius: 55px;
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #d6edfa 100%);
|
||||
padding: 18px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.cp-label {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #0088cc;
|
||||
line-height: 1.4;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.cp-sub {
|
||||
font-size: 12px;
|
||||
color: #0088cc;
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-line;
|
||||
font-weight: 500;
|
||||
}
|
||||
.cp-vs {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #333;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
34
templates/blocks/visuals/flow-arrow-horizontal.html
Normal file
34
templates/blocks/visuals/flow-arrow-horizontal.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!-- 가로 흐름도: 좌→우 화살표로 연결된 단계 (SVG) -->
|
||||
<!--
|
||||
📋 flow-arrow-horizontal
|
||||
─────────────────
|
||||
용도: 발전 과정, 기술 진화, 전환 흐름 (A → B → C)
|
||||
슬롯: steps[] (각 단계에 label, sub)
|
||||
Figma 원본: 2-3_03 "GIS ↔ SPCC → 토공 → ..." / 2-2_04 개발 패러다임
|
||||
-->
|
||||
<div class="block-flow-h">
|
||||
<svg viewBox="0 0 {{ steps|length * 180 }} 80" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
{% for step in steps %}
|
||||
{% set x = loop.index0 * 180 + 70 %}
|
||||
<rect x="{{ x - 60 }}" y="10" width="120" height="50" rx="25" fill="{{ step.color | default('#2563eb') }}" opacity="0.9" />
|
||||
<text x="{{ x }}" y="32" text-anchor="middle" fill="white" font-size="13" font-weight="700">{{ step.label }}</text>
|
||||
{% if step.sub %}
|
||||
<text x="{{ x }}" y="48" text-anchor="middle" fill="white" font-size="10" opacity="0.8">{{ step.sub }}</text>
|
||||
{% endif %}
|
||||
{% if not loop.last %}
|
||||
<polygon points="{{ x + 65 }},35 {{ x + 80 }},35 {{ x + 75 }},28" fill="#94a3b8" />
|
||||
<polygon points="{{ x + 65 }},35 {{ x + 80 }},35 {{ x + 75 }},42" fill="#94a3b8" />
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-flow-h {
|
||||
padding: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.block-flow-h svg {
|
||||
min-width: 400px;
|
||||
}
|
||||
</style>
|
||||
56
templates/blocks/visuals/keyword-circle-row.html
Normal file
56
templates/blocks/visuals/keyword-circle-row.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!-- 키워드 원형 행: 원 안에 키워드 + 하단 설명 (SVG) -->
|
||||
<!--
|
||||
📋 keyword-circle-row
|
||||
─────────────────
|
||||
용도: 핵심 키워드를 원형으로 나열하며 각각 설명. 약어 풀이.
|
||||
슬롯: keywords[] (각 항목에 letter, label, description, color)
|
||||
Figma 원본: 2-3_03 G-S-I-M 약어 설명, 2-2_04 개발 조건 키워드
|
||||
-->
|
||||
<div class="block-kw-circles">
|
||||
{% for kw in keywords %}
|
||||
<div class="kw-item">
|
||||
<svg viewBox="0 0 80 80" width="70" height="70" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="kwGrad{{ loop.index }}" cx="40%" cy="35%" r="60%">
|
||||
<stop offset="0%" stop-color="{{ kw.color_light | default('#93c5fd') }}" />
|
||||
<stop offset="100%" stop-color="{{ kw.color | default('#2563eb') }}" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="40" cy="40" r="38" fill="url(#kwGrad{{ loop.index }})" />
|
||||
<text x="40" y="44" text-anchor="middle" dominant-baseline="central" fill="white" font-size="28" font-weight="900" font-family="Pretendard Variable, sans-serif">{{ kw.letter }}</text>
|
||||
</svg>
|
||||
<div class="kw-label">{{ kw.label }}</div>
|
||||
{% if kw.description %}<div class="kw-desc">{{ kw.description }}</div>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-kw-circles {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.kw-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 140px;
|
||||
}
|
||||
.kw-label {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.kw-desc {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
50
templates/blocks/visuals/layer-diagram.html
Normal file
50
templates/blocks/visuals/layer-diagram.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!-- 레이어 다이어그램: 겹쳐진 레이어 표현 (SVG) -->
|
||||
<!--
|
||||
📋 layer-diagram
|
||||
─────────────────
|
||||
용도: GIS/BIM/DT 레이어 구조, 기술 스택, 계층 구조 시각화
|
||||
슬롯: layers[] (각 레이어에 label, color), title (선택)
|
||||
Figma 원본: 1장_1-1_미래 "GIS+BIM+DT 레이어 시각화"
|
||||
-->
|
||||
<div class="block-layer-diag">
|
||||
{% if title %}<div class="ld-title">{{ title }}</div>{% endif %}
|
||||
<svg viewBox="0 0 400 {{ layers|length * 60 + 40 }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
{% for layer in layers %}
|
||||
<linearGradient id="layerGrad{{ loop.index }}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="{{ layer.color | default('#2563eb') }}" stop-opacity="0.85" />
|
||||
<stop offset="100%" stop-color="{{ layer.color | default('#2563eb') }}" stop-opacity="0.6" />
|
||||
</linearGradient>
|
||||
{% endfor %}
|
||||
<filter id="layerShadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.15" />
|
||||
</filter>
|
||||
</defs>
|
||||
{% for layer in layers %}
|
||||
{% set y = (layers|length - loop.index) * 55 + 20 %}
|
||||
{% set offset = loop.index0 * 15 %}
|
||||
<!-- 3D 효과: 사다리꼴 레이어 -->
|
||||
<path d="M {{ 40 + offset }},{{ y }} L {{ 360 - offset }},{{ y }} L {{ 340 - offset }},{{ y + 40 }} L {{ 60 + offset }},{{ y + 40 }} Z"
|
||||
fill="url(#layerGrad{{ loop.index }})" filter="url(#layerShadow)" />
|
||||
<text x="200" y="{{ y + 25 }}" text-anchor="middle" fill="white" font-size="14" font-weight="700">{{ layer.label }}</text>
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-layer-diag {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.ld-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
.block-layer-diag svg {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
61
templates/blocks/visuals/process-horizontal.html
Normal file
61
templates/blocks/visuals/process-horizontal.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!-- 프로세스 블록: 가로 단계 흐름 -->
|
||||
<div class="block-process">
|
||||
{% for step in steps %}
|
||||
<div class="process-step">
|
||||
<div class="process-number">{{ step.number | default(loop.index) }}</div>
|
||||
<div class="process-title">{{ step.title }}</div>
|
||||
{% if step.description %}<div class="process-desc">{{ step.description }}</div>{% endif %}
|
||||
</div>
|
||||
{% if not loop.last %}
|
||||
<div class="process-arrow">→</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-process {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-small);
|
||||
height: 100%;
|
||||
}
|
||||
.process-step {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: var(--spacing-inner);
|
||||
background: var(--color-bg-subtle);
|
||||
border: var(--border-width) solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.process-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--weight-bold);
|
||||
font-size: var(--font-body);
|
||||
margin: 0 auto var(--spacing-small);
|
||||
}
|
||||
.process-title {
|
||||
font-size: var(--font-body);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.process-desc {
|
||||
font-size: var(--font-caption);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-ko);
|
||||
}
|
||||
.process-arrow {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--weight-bold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
40
templates/blocks/visuals/pyramid-hierarchy.html
Normal file
40
templates/blocks/visuals/pyramid-hierarchy.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!-- 피라미드 계층: 위에서 아래로 넓어지는 계층 구조 (SVG) -->
|
||||
<!--
|
||||
📋 pyramid-hierarchy
|
||||
─────────────────
|
||||
용도: 위계, 우선순위, 상위→하위 개념 (좁은→넓은)
|
||||
슬롯: levels[] (상단부터, 각 레벨에 label, color)
|
||||
-->
|
||||
<div class="block-pyramid">
|
||||
<svg viewBox="0 0 500 {{ levels|length * 70 + 20 }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
{% for level in levels %}
|
||||
<linearGradient id="pyrGrad{{ loop.index }}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="{{ level.color | default('#2563eb') }}" />
|
||||
<stop offset="100%" stop-color="{{ level.color | default('#2563eb') }}" stop-opacity="0.7" />
|
||||
</linearGradient>
|
||||
{% endfor %}
|
||||
<filter id="pyrShadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.12" />
|
||||
</filter>
|
||||
</defs>
|
||||
{% for level in levels %}
|
||||
{% set i = loop.index0 %}
|
||||
{% set y = i * 65 + 10 %}
|
||||
{% set w_half = 60 + i * 55 %}
|
||||
<rect x="{{ 250 - w_half }}" y="{{ y }}" width="{{ w_half * 2 }}" height="50" rx="6" fill="url(#pyrGrad{{ loop.index }})" filter="url(#pyrShadow)" />
|
||||
<text x="250" y="{{ y + 30 }}" text-anchor="middle" fill="white" font-size="14" font-weight="700">{{ level.label }}</text>
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-pyramid {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.block-pyramid svg {
|
||||
max-width: 450px;
|
||||
}
|
||||
</style>
|
||||
37
templates/blocks/visuals/timeline-horizontal.html
Normal file
37
templates/blocks/visuals/timeline-horizontal.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!-- 가로 타임라인: 좌→우 시간축 + 마커 + 라벨 (SVG) -->
|
||||
<!--
|
||||
📋 timeline-horizontal
|
||||
─────────────────
|
||||
용도: 연도별 로드맵, 짧은 일정, 마일스톤 (가로 배치)
|
||||
슬롯: events[] (각 이벤트에 year, title, color)
|
||||
timeline-vertical과 다른 점: 가로 방향, 공간 효율적
|
||||
-->
|
||||
<div class="block-timeline-h">
|
||||
<svg viewBox="0 0 {{ events|length * 160 + 40 }} 100" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<!-- 가로 선 -->
|
||||
<line x1="30" y1="40" x2="{{ events|length * 160 - 10 }}" y2="40" stroke="#cbd5e1" stroke-width="2" />
|
||||
{% for event in events %}
|
||||
{% set x = loop.index0 * 160 + 60 %}
|
||||
<!-- 마커 -->
|
||||
<circle cx="{{ x }}" cy="40" r="12" fill="{{ event.color | default('#2563eb') }}" />
|
||||
<circle cx="{{ x }}" cy="40" r="5" fill="white" />
|
||||
<!-- 연도 -->
|
||||
<text x="{{ x }}" y="22" text-anchor="middle" fill="{{ event.color | default('#2563eb') }}" font-size="12" font-weight="800">{{ event.year }}</text>
|
||||
<!-- 제목 -->
|
||||
<text x="{{ x }}" y="65" text-anchor="middle" fill="#1e293b" font-size="12" font-weight="600">{{ event.title }}</text>
|
||||
{% if event.sub %}
|
||||
<text x="{{ x }}" y="80" text-anchor="middle" fill="#64748b" font-size="10">{{ event.sub }}</text>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-timeline-h {
|
||||
padding: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.block-timeline-h svg {
|
||||
min-width: 500px;
|
||||
}
|
||||
</style>
|
||||
74
templates/blocks/visuals/timeline-vertical.html
Normal file
74
templates/blocks/visuals/timeline-vertical.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!-- 세로 타임라인: 좌측 선 + 원형 마커 + 우측 내용 (SVG 마커) -->
|
||||
<!--
|
||||
📋 timeline-vertical
|
||||
─────────────────
|
||||
용도: 연혁, 정책 시행 일정, 로드맵, 연도별 사건
|
||||
슬롯: events[] (각 이벤트에 year, title, description, color)
|
||||
Figma 참고: 정책 로드맵, 건설 정책 추진현황
|
||||
-->
|
||||
<div class="block-timeline-v">
|
||||
{% for event in events %}
|
||||
<div class="tv-event">
|
||||
<div class="tv-marker-col">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" fill="{{ event.color | default('#2563eb') }}" />
|
||||
<circle cx="12" cy="12" r="5" fill="white" />
|
||||
</svg>
|
||||
{% if not loop.last %}<div class="tv-line" style="background: {{ event.color | default('#2563eb') }}"></div>{% endif %}
|
||||
</div>
|
||||
<div class="tv-content">
|
||||
<div class="tv-year" style="color: {{ event.color | default('#2563eb') }}">{{ event.year }}</div>
|
||||
<div class="tv-title">{{ event.title }}</div>
|
||||
{% if event.description %}<div class="tv-desc">{{ event.description }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-timeline-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.tv-event {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
}
|
||||
.tv-marker-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
}
|
||||
.tv-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
opacity: 0.3;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.tv-content {
|
||||
padding-bottom: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.tv-year {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.tv-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tv-desc {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
125
templates/blocks/visuals/venn-diagram.html
Normal file
125
templates/blocks/visuals/venn-diagram.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!-- 벤 다이어그램: SVG premium (N개 자동 배치) -->
|
||||
<!--
|
||||
📋 venn-diagram (P2-B: 동적 좌표 계산)
|
||||
─────────────────
|
||||
용도: 상위-하위 포함 관계, 기술 융합 구조
|
||||
방식: renderer가 svg_calculator.prepare_venn_data()로 좌표 사전 계산
|
||||
→ items[].cx, cy, r + outer_r, center_x, center_y 전달
|
||||
Phase 1 fallback: outer_r이 없으면 3개 고정 좌표 사용
|
||||
-->
|
||||
<div class="block-venn-svg">
|
||||
{% if outer_r is defined %}
|
||||
{# ═══ P2-B: 동적 N개 배치 ═══ #}
|
||||
<svg viewBox="0 0 {{ viewbox_width | default(600) }} {{ viewbox_height | default(550) }}" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#dce3ea" />
|
||||
<stop offset="100%" stop-color="#f0f2f5" />
|
||||
</linearGradient>
|
||||
<radialGradient id="blueOuter" cx="45%" cy="40%" r="55%">
|
||||
<stop offset="0%" stop-color="#2979ff" />
|
||||
<stop offset="40%" stop-color="#1565c0" />
|
||||
<stop offset="100%" stop-color="#0d47a1" />
|
||||
</radialGradient>
|
||||
<radialGradient id="hlGrad" cx="35%" cy="25%" r="40%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.5)" />
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)" />
|
||||
</radialGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur" />
|
||||
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
|
||||
</filter>
|
||||
<filter id="shadow">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.3)" />
|
||||
</filter>
|
||||
{% for item in items %}
|
||||
<radialGradient id="itemGrad{{ loop.index }}" cx="35%" cy="30%" r="65%">
|
||||
<stop offset="0%" stop-color="{{ item.color_light | default('#93c5fd') }}" />
|
||||
<stop offset="100%" stop-color="{{ item.color | default('#3b82f6') }}" />
|
||||
</radialGradient>
|
||||
{% endfor %}
|
||||
</defs>
|
||||
|
||||
<rect width="{{ viewbox_width | default(600) }}" height="{{ viewbox_height | default(550) }}" fill="url(#bgGrad)" />
|
||||
|
||||
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r }}" fill="url(#blueOuter)" filter="url(#shadow)" />
|
||||
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 10 }}" fill="none" stroke="#4a90d9" stroke-width="1.5" opacity="0.4" />
|
||||
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 25 }}" fill="none" stroke="#5a9de0" stroke-width="1" opacity="0.3" />
|
||||
<circle cx="{{ center_x }}" cy="{{ center_y }}" r="{{ outer_r - 40 }}" fill="none" stroke="#6aabe6" stroke-width="0.8" opacity="0.25" />
|
||||
|
||||
{% for item in items %}
|
||||
<circle cx="{{ item.cx }}" cy="{{ item.cy }}" r="{{ item.r }}" fill="url(#itemGrad{{ loop.index }})" opacity="0.9" filter="url(#glow)" />
|
||||
<circle cx="{{ item.cx }}" cy="{{ item.cy }}" r="{{ item.r }}" fill="url(#hlGrad)" />
|
||||
<text x="{{ item.cx }}" y="{{ item.cy }}" text-anchor="middle" dominant-baseline="central" fill="white" font-size="{% if item.r > 60 %}18{% elif item.r > 40 %}15{% else %}12{% endif %}" font-weight="700">{{ item.label }}</text>
|
||||
{% if item.sub %}
|
||||
<text x="{{ item.cx }}" y="{{ item.cy + 18 }}" text-anchor="middle" fill="white" font-size="11" opacity="0.85">{{ item.sub }}</text>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<text x="{{ center_x }}" y="{{ center_y - outer_r + 35 }}" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="400" opacity="0.85">{{ center_sub | default('') }}</text>
|
||||
<text x="{{ center_x }}" y="{{ center_y - outer_r + 60 }}" text-anchor="middle" fill="#ffffff" font-size="24" font-weight="900">{{ center_label }}</text>
|
||||
</svg>
|
||||
|
||||
{% else %}
|
||||
{# ═══ Phase 1 fallback: 3개 고정 ═══ #}
|
||||
<svg viewBox="0 0 600 550" width="100%" xmlns="http://www.w3.org/2000/svg" font-family="Pretendard Variable, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#dce3ea" /><stop offset="100%" stop-color="#f0f2f5" /></linearGradient>
|
||||
<radialGradient id="blueOuter" cx="45%" cy="40%" r="55%"><stop offset="0%" stop-color="#2979ff" /><stop offset="40%" stop-color="#1565c0" /><stop offset="100%" stop-color="#0d47a1" /></radialGradient>
|
||||
<radialGradient id="orangeGrad" cx="35%" cy="35%" r="65%"><stop offset="0%" stop-color="#ffab40" /><stop offset="40%" stop-color="#ff6d00" /><stop offset="100%" stop-color="#c2185b" /></radialGradient>
|
||||
<radialGradient id="mintGrad" cx="40%" cy="30%" r="65%"><stop offset="0%" stop-color="#80deea" /><stop offset="40%" stop-color="#26c6da" /><stop offset="100%" stop-color="#00897b" /></radialGradient>
|
||||
<radialGradient id="goldGrad" cx="40%" cy="30%" r="65%"><stop offset="0%" stop-color="#ffd54f" /><stop offset="40%" stop-color="#ffb300" /><stop offset="100%" stop-color="#e65100" /></radialGradient>
|
||||
<radialGradient id="hlGrad" cx="35%" cy="25%" r="40%"><stop offset="0%" stop-color="rgba(255,255,255,0.5)" /><stop offset="100%" stop-color="rgba(255,255,255,0)" /></radialGradient>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="8" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>
|
||||
<filter id="shadow"><feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="rgba(0,0,0,0.3)" /></filter>
|
||||
</defs>
|
||||
<rect width="600" height="550" fill="url(#bgGrad)" />
|
||||
<circle cx="300" cy="270" r="230" fill="url(#blueOuter)" filter="url(#shadow)" />
|
||||
<circle cx="300" cy="270" r="220" fill="none" stroke="#4a90d9" stroke-width="1.5" opacity="0.4" />
|
||||
<circle cx="300" cy="270" r="205" fill="none" stroke="#5a9de0" stroke-width="1" opacity="0.3" />
|
||||
<circle cx="300" cy="270" r="190" fill="none" stroke="#6aabe6" stroke-width="0.8" opacity="0.25" />
|
||||
<circle cx="265" cy="300" r="120" fill="url(#orangeGrad)" opacity="0.92" filter="url(#glow)" />
|
||||
<circle cx="265" cy="300" r="120" fill="url(#hlGrad)" />
|
||||
<circle cx="370" cy="230" r="75" fill="url(#mintGrad)" opacity="0.9" filter="url(#glow)" />
|
||||
<circle cx="370" cy="230" r="75" fill="url(#hlGrad)" />
|
||||
<circle cx="365" cy="355" r="75" fill="url(#goldGrad)" opacity="0.9" filter="url(#glow)" />
|
||||
<circle cx="365" cy="355" r="75" fill="url(#hlGrad)" />
|
||||
<text x="300" y="95" text-anchor="middle" fill="#ffffff" font-size="13" font-weight="400" opacity="0.85">{{ center_sub | default('') }}</text>
|
||||
<text x="300" y="125" text-anchor="middle" fill="#ffffff" font-size="26" font-weight="900">{{ center_label }}</text>
|
||||
{% if items and items | length > 0 %}<text x="250" y="295" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[0].label }}</text>{% if items[0].sub %}<text x="250" y="318" text-anchor="middle" fill="#ffffff" font-size="13" opacity="0.85">{{ items[0].sub }}</text>{% endif %}{% endif %}
|
||||
{% if items and items | length > 1 %}<text x="370" y="237" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[1].label }}</text>{% endif %}
|
||||
{% if items and items | length > 2 %}<text x="365" y="362" text-anchor="middle" fill="#ffffff" font-size="20" font-weight="800">{{ items[2].label }}</text>{% endif %}
|
||||
</svg>
|
||||
{% endif %}
|
||||
|
||||
{% if description %}
|
||||
<div class="venn-desc">{{ description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-venn-svg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
flex-shrink: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.block-venn-svg svg {
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.venn-desc {
|
||||
font-size: 14px;
|
||||
color: #444;
|
||||
text-align: center;
|
||||
max-width: 450px;
|
||||
line-height: 1.7;
|
||||
word-break: keep-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user