On main: pre-Stage3 IMP-09 B-4 — out-of-scope pre-existing modifications (preserve for later)

This commit is contained in:
2026-05-17 04:28:05 +09:00
11 changed files with 2573 additions and 891 deletions
+2
View File
@@ -7,6 +7,8 @@ __pycache__/
dist/ dist/
build/ build/
.venv/ .venv/
.orchestrator/
tmpim*.json/
node_modules/ node_modules/
data/ data/
+25 -198
View File
@@ -5,205 +5,32 @@ sidebar:
--- ---
## 1. DX에 대한 인식 ## 1. DX에 대한 인식
<br/>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', justifycontent: 'center' }}>
<div style={{ - **기술 및 소프트웨어 이해도**
width: '100%', - "무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"
minWidth: '300px', - 기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.
background: '#fff', - 3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.
border: '1px solid #e1e4e8', - 필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?
borderRadius: '12px', - **효과와 효율성**
padding: '20px', - "2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"
boxShadow: '0 4px 6px rgba(0,0,0,0.05)', - 성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.
position: 'relative', - BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.
transition: 'transform 0.2s, box-shadow 0.2s', - 결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?
cursor: 'default' - **인력 및 교육**
}} - "수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"
onMouseOver={(e) => { - 교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.
e.currentTarget.style.transform = 'translateY(-3px)'; - 신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)'; - 여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.
}} - **경제적 부담**
onMouseOut={(e) => { - "S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"
e.currentTarget.style.transform = 'translateY(0)'; - 사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)'; - 모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.
}} - 디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?
> - **실무 및 적용성**
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}> - "실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>기술 및 소프트웨어 이해도</h3> - 구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.
</div> - 지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.
<p style={{ - 속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.
background: '#f8f9fa',
borderLeft: '4px solid #1a73e8',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.</li>
<li>3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.</li>
<li>필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>효과와 효율성</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #d93025',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.</li>
<li>BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.</li>
<li>결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>인력 및 교육</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #f9ab00',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.</li>
<li>신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.</li>
<li>여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>경제적 부담</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #34a853',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?</li>
<li>모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.</li>
<li>디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?</li>
</ul>
</div>
<div style={{
width: '100%',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default',
marginTop: '10px'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>실무 및 적용성</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #8e24aa',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.</li>
<li>지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.</li>
<li>속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.</li>
</ul>
</div>
</div>
<br/> <br/>
--- ---
<br/> <br/>
+25 -198
View File
@@ -5,205 +5,32 @@ sidebar:
--- ---
## 1. DX에 대한 인식 ## 1. DX에 대한 인식
<br/>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', justifycontent: 'center' }}>
<div style={{ - **기술 및 소프트웨어 이해도**
width: '100%', - "무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"
minWidth: '300px', - 기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.
background: '#fff', - 3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.
border: '1px solid #e1e4e8', - 필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?
borderRadius: '12px', - **효과와 효율성**
padding: '20px', - "2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"
boxShadow: '0 4px 6px rgba(0,0,0,0.05)', - 성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.
position: 'relative', - BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.
transition: 'transform 0.2s, box-shadow 0.2s', - 결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?
cursor: 'default' - **인력 및 교육**
}} - "수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"
onMouseOver={(e) => { - 교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.
e.currentTarget.style.transform = 'translateY(-3px)'; - 신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)'; - 여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.
}} - **경제적 부담**
onMouseOut={(e) => { - "S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"
e.currentTarget.style.transform = 'translateY(0)'; - 사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)'; - 모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.
}} - 디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?
> - **실무 및 적용성**
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}> - "실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>기술 및 소프트웨어 이해도</h3> - 구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.
</div> - 지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.
<p style={{ - 속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.
background: '#f8f9fa',
borderLeft: '4px solid #1a73e8',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.</li>
<li>3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.</li>
<li>필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>효과와 효율성</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #d93025',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.</li>
<li>BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.</li>
<li>결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>인력 및 교육</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #f9ab00',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.</li>
<li>신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.</li>
<li>여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.</li>
</ul>
</div>
<div style={{
width: '100%',
minWidth: '300px',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>경제적 부담</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #34a853',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?</li>
<li>모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.</li>
<li>디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?</li>
</ul>
</div>
<div style={{
width: '100%',
background: '#fff',
border: '1px solid #e1e4e8',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.05)',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'default',
marginTop: '10px'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-3px)';
e.currentTarget.style.boxShadow = '0 6px 12px rgba(0,0,0,0.1)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 6px rgba(0,0,0,0.05)';
}}
>
<div style={{ display: 'flex', alignitems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0, fontSize: '1.25rem'}}>실무 및 적용성</h3>
</div>
<p style={{
background: '#f8f9fa',
borderLeft: '4px solid #8e24aa',
padding: '10px 15px',
margin: '0 0 15px 0',
fontSize: '1rem',
fontWeight: 'bold',
color: '#333'
}}>"실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"</p>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#555', fontSize: '1.0rem', lineHeight: '1.8' }}>
<li>구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.</li>
<li>지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.</li>
<li>속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.</li>
</ul>
</div>
</div>
<br/> <br/>
--- ---
<br/> <br/>
+21 -2
View File
@@ -383,8 +383,20 @@ def _build_process_product_pair(section, units, contract) -> dict:
payload: dict = {} payload: dict = {}
payload.update(_resolve_title(section, contract["payload"], contract)) payload.update(_resolve_title(section, contract["payload"], contract))
# 2026-05-14 — popup link extraction. column body 안 `<details><summary>X</summary>` regex.
# 사용자 lock : 04-2 같은 통합 column 의 popup link 를 파란색 글씨 indicator 로 표시.
# popup 본 콘텐츠 (img) 는 별 axis — 본 mapping 은 summary text 만.
popup_pattern = re.compile(r"<details[^>]*>\s*<summary[^>]*>(.+?)</summary>", re.DOTALL)
# column index → popup slot name mapping (0 = left, 1 = right).
popup_slot_names = ["popup_left_summary", "popup_right_summary"]
for i, col in enumerate(cols): for i, col in enumerate(cols):
sub_title, sub_body = units[i] sub_title, sub_body = units[i]
# popup summary extraction (column body 안에서 가장 가까운 details 의 summary).
m = popup_pattern.search(sub_body)
if m and i < len(popup_slot_names):
payload[popup_slot_names[i]] = m.group(1).strip()
parser_name = col["body_parser"] parser_name = col["body_parser"]
parser = COLUMN_BODY_PARSERS.get(parser_name) parser = COLUMN_BODY_PARSERS.get(parser_name)
if parser is None: if parser is None:
@@ -428,8 +440,12 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
f"but ITEM_PARSERS has no such entry." f"but ITEM_PARSERS has no such entry."
) )
pad_to = options.get("pad_to", 4) # 2026-05-14 — pad_to / truncate_at 가 null 이면 dynamic (콘텐츠 cardinality 그대로).
truncate_at = options.get("truncate_at", pad_to) # frame 이 콘텐츠 cardinality 에 맞춰 보완·수정 룰. 기존 4 quadrant frame 들은
# default 4 유지 → backward compatible.
raw_pad_to = options.get("pad_to", 4)
raw_truncate_at = options.get("truncate_at", raw_pad_to)
truncate_at = len(units) if raw_truncate_at is None else raw_truncate_at
label_key = options.get("label_key_pattern", "quadrant_{n}_label") label_key = options.get("label_key_pattern", "quadrant_{n}_label")
body_key = options.get("body_key_pattern", "quadrant_{n}_body") body_key = options.get("body_key_pattern", "quadrant_{n}_body")
empty_label = options.get("empty_label", "") empty_label = options.get("empty_label", "")
@@ -440,6 +456,7 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
visible_units = list(units[:truncate_at]) visible_units = list(units[:truncate_at])
parsed = [parser(u) for u in visible_units] parsed = [parser(u) for u in visible_units]
pad_to = len(parsed) if raw_pad_to is None else raw_pad_to
for i in range(pad_to): for i in range(pad_to):
n = i + 1 n = i + 1
@@ -453,6 +470,8 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
if len(units) > truncate_at: if len(units) > truncate_at:
payload["_truncated_count"] = len(units) - truncate_at payload["_truncated_count"] = len(units) - truncate_at
# dynamic count for partial template 동적 loop
payload["_slot_count"] = pad_to
return payload return payload
+134 -19
View File
@@ -84,7 +84,7 @@ process_product_two_way:
strict: 2 # F29 frame = 2 visual columns. ≠2 → fallback. strict: 2 # F29 frame = 2 visual columns. ≠2 → fallback.
overflow_policy: abort_or_review overflow_policy: abort_or_review
# transform-block + 3 sections 의 breathing 위해 345 (이전 hardcoded 동일). # transform-block + 3 sections 의 breathing 위해 345 (원래 hardcoded 동일).
visual_hints: visual_hints:
min_height_px: 345 min_height_px: 345
@@ -98,16 +98,19 @@ process_product_two_way:
# NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언. # NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언.
# 2 column × 3 section = 6 cell, but Frame Slot 단위 = column. # 2 column × 3 section = 6 cell, but Frame Slot 단위 = column.
# partial_target_path = path 식별자 만 (값). marker attribute 결정은 별 axis. # partial_target_path = path 식별자 만 (값). marker attribute 결정은 별 axis.
# 2026-05-14 — cardinality dynamic 화. 사용자 룰 : frame 이 콘텐츠 cardinality 에
# 맞춰 보완·수정. 03 (3 cards) / 04-2 (4 cards) / 미래 N cards 모두 동일 frame 으로
# 자동 대응. strict 제거 → null = 동적 max 미정.
sub_zones: sub_zones:
- id: process_column - id: process_column
role: main_text role: main_text
accepts: [text_block, transform_table] accepts: [text_block, transform_table]
cardinality: { strict: 3 } # 3 sections per column cardinality: { strict: null }
partial_target_path: ".f29b__grid .f29b__cell--left" partial_target_path: ".f29b__grid .f29b__cell--left"
- id: product_column - id: product_column
role: main_text role: main_text
accepts: [text_block] # product 쪽은 transform 안 받음 (현재 frame 의 시각 구분) accepts: [text_block]
cardinality: { strict: 3 } cardinality: { strict: null }
partial_target_path: ".f29b__grid .f29b__cell--right" partial_target_path: ".f29b__grid .f29b__cell--right"
payload: payload:
@@ -115,7 +118,8 @@ process_product_two_way:
source: section.title source: section.title
builder: process_product_pair builder: process_product_pair
builder_options: builder_options:
pad_sections_to: 3 # 각 column.sections 길이 = 3 (legacy 와 동일) # pad_sections_to null → builder 가 pad / truncate 안 함. 콘텐츠 원본 cardinality 보존.
pad_sections_to: null
columns: columns:
- title_to: banner_left - title_to: banner_left
body_to: process body_to: process
@@ -397,11 +401,14 @@ bim_dx_comparison_table:
frame_id: 1171281195 frame_id: 1171281195
family: table family: table
source_shape: top_bullets # 2026-05-14 — source_shape 변경 : top_bullets → h3_subsections.
# NOTE (Codex round 43 §F1-c) : top-level `cardinality.strict: 2` = *column 수* # 사용자 룰 (frame 보완·수정) : ranking 에 나온 frame 은 mdx 콘텐츠 구조에 맞게
# (col_a / col_b). data row 수 는 별 — `sub_zones.rows.cardinality` 의 `{min:1, max:12}`. # frame 의 builder/partial 보완. 04-2 의 통합 콘텐츠 (### 2.1 정책 + ### 2.2 조직) 가
# 2 column 으로 자연 매핑되도록 source_shape 변경 + builder = process_product_pair 재사용.
# frame 의 visual identity (갈색 그라데이션 / zebra / table 시각) 는 partial 에서 유지.
source_shape: h3_subsections
cardinality: cardinality:
strict: 2 # 2 columns (col_a / col_b) — NOT row count strict: 2 # 2 h3 subsections (col_a / col_b)
overflow_policy: abort_or_review overflow_policy: abort_or_review
role_order: role_order:
@@ -440,17 +447,81 @@ bim_dx_comparison_table:
payload: payload:
title: title:
source: section.title source: section.title
# NEW builder `compare_table_2col` (phase_z2_mapper.py) + # 2026-05-14 — builder 변경 : compare_table_2col → process_product_pair 재사용.
# NEW parser `compare_row_2col_item` (ITEM_PARSERS). # 사용자 룰 (frame 보완) : 04-2 같은 통합 콘텐츠 (### 2 h3 + 각 N top_bullets) 가
builder: compare_table_2col # 2 column N items 로 자연 매핑. frame 의 갈색 visual + zebra 는 partial 에서 유지.
# col_a_label_default / col_b_label_default 제거 — h3 title 그대로 사용 (BIM/DX 강제 없음).
builder: process_product_pair
builder_options: builder_options:
item_parser: compare_row_2col_item # NEW parser — top_bullet → {label, col_a, col_b} pad_sections_to: null # dynamic — 각 column N items 그대로
col_a_label_default: "BIM" # F1-a (Codex round 43) — explicit default columns:
col_b_label_default: "DX" # F1-a — explicit default - title_to: col_a_label
strip_col_prefix_aliases: # F1-b (Codex round 43) — narrow alias 만 strip body_to: col_a_body
- "BIM" # 2026-05-15 — 03-2 의 markdown table (AS-IS/TO-BE) 보존. column_plain
- "DX" # 은 transforms 무시 → 사용자 본 "AS-IS/TO-BE 사라짐". with_transform
max_rows: 12 # typical 4-8, overflow 보호 # 으로 변경하면 첫 top-bullet 안 markdown table 을 transforms 로 추출.
# 04-2 같은 transforms 없는 콘텐츠는 fallback 으로 일반 text_lines.
body_parser: column_with_transform
- title_to: col_b_label
body_to: col_b_body
body_parser: column_with_transform
app_sw_package_vs_solution:
# 2026-05-15 — catalog 등록 (frame 23). 03-2 의 V4 rank 3 (reject 0.647).
# 사용자 룰 = rank 마다 frame visual 은 달라도 mdx 콘텐츠는 동일 / 무손실.
# builder + parser = bim_dx_comparison_table 와 동일 (process_product_pair +
# column_with_transform) — markdown table (AS-IS/TO-BE) 보존. partial 만 frame 23
# 의 visual identity (label col + 2 data cols, teal/orange header, zebra rows).
template_id: app_sw_package_vs_solution
frame_id: 1171281203
family: table
source_shape: h3_subsections
cardinality:
strict: 2
overflow_policy: abort_or_review
role_order:
- col_a
- col_b
visual_hints:
min_height_px: 350
accepted_content_types:
- text_block
sub_zones:
- id: col_a_header
role: header
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f23b__th--a"
- id: col_b_header
role: header
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f23b__th--b"
- id: rows
role: data
accepts: [text_block]
cardinality: { min: 1, max: 12 }
partial_target_path: ".f23b__rows"
payload:
title:
source: section.title
builder: process_product_pair
builder_options:
pad_sections_to: null
columns:
- title_to: col_a_label
body_to: col_a_body
body_parser: column_with_transform
- title_to: col_b_label
body_to: col_b_body
body_parser: column_with_transform
dx_sw_necessity_three_perspectives: dx_sw_necessity_three_perspectives:
@@ -727,3 +798,47 @@ bim_current_problems_paired:
body_key_pattern: "row_{r}_{side}_body" body_key_pattern: "row_{r}_{side}_body"
rows: 4 rows: 4
sides: [left, right] sides: [left, right]
# 2026-05-14 신규 — V4 04-1 top conf=0.722 rank-1 매칭 frame. ideal 5 / min 4 / max 6.
# Frame 9 visual : stacked pills (계단 배치, 5색), 좌측 vertical label, 타이틀 bar.
# 보완 (사용자 룰) : pill 안 nested body slot 추가 (1 라인 frame 9 → multi-line pill)
# 으로 04-1 의 5 cards × (인용구 + 3 bullets) 콘텐츠 보존.
pre_construction_model_info_stacked:
template_id: pre_construction_model_info_stacked
frame_id: 1171281180
family: stacked_list
source_shape: top_bullets
cardinality:
strict: null # dynamic (ideal 5 / min 4 / max 6 per Figma analysis)
overflow_policy: abort_or_review
# 2026-05-14 — frame 9 의 catalog default = frame visual 자체 (모든 mdx 공통).
# pill horizontal 변환 시 frame visual 보완값 = 250. 슬라이드 단위 zone 크기 변경은
# catalog 가 아닌 run-time override (zone_geometry 등) 로 처리해야 함.
visual_hints:
min_height_px: 250
accepted_content_types:
- text_block
# dynamic sub_zones (N pills) — N = 콘텐츠 cardinality 그대로.
# partial_target_path = nth-child selector pattern (frontend / future override 용).
sub_zones:
- id: pill_dynamic
role: main_text
accepts: [text_block]
cardinality: { strict: null }
partial_target_path: ".f9b__pill-rows .f9b__pill:nth-child(N)"
payload:
title:
source: section.title
builder: quadrant_flat_slots # 재사용 — pad_to/truncate_at null 시 dynamic
builder_options:
item_parser: quadrant_item
pad_to: null # null = dynamic (콘텐츠 N 그대로)
truncate_at: null # null = truncate 안 함
label_key_pattern: "pill_{n}_label"
body_key_pattern: "pill_{n}_body"
@@ -1,53 +1,35 @@
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block. <!-- Phase Z-2 frame partial — Frame 18 (1171281195) bim_dx_comparison_table.
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. --> 2026-05-14 — Figma 원본 visual (color/structure/badge/typography) 정확 반영 후 보완.
사용자 catch : 이전 partial 이 figma 원본과 색감/구조 달랐음. 본 partial 재작성.
visual = 3-col compare-row layout (col-left brown / col-badge pill green-brown / col-right green).
보완 (사용자 룰) : col_a/col_b body = N items 그대로 (04-2 통합 콘텐츠 정책 4 + 조직 4
같은 경우도 자연 매핑). badge = row 번호 자동 (figma 원본의 가운데 axis label 자리). -->
{# {#
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
Visual Provenance — figma_to_html_agent/blocks/1171281195/ (frame 18) Visual Provenance — figma_to_html_agent/blocks/1171281195/index.html
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
Frame 18 = "BIM 과 DX 의 이해" (table / compare-rows). 2개 column header (BIM vs DX) + PROMOTED (Figma 1:1, scale 1280/1868 = 0.68524) :
중앙 category column + N category rows (범위 / S/W / 프로세스 / 성과물 / 활용 / - title gradient : linear-gradient(rgb(204,82,0), rgb(136,55,0))
확장성 / 수행개념 / 주체 등). compare 관점의 다면 비교 표 디자인. - subheader bar : pill (border-radius: 50px), 좌=green / 우=brown gradient
- col-left color : #5c3714 (brown family)
- col-right color : #285b4a (deep green family)
- col-badge : 둥근 사각형 (radius 10px), 그라데이션 green↔brown, 흰 글자
- bullets : list-style disc inside (filled bullets)
- typography weight : 700 전체
본 partial = Track A frame 4 (Codex round 41 accepted, V4 UAI tier). NEW builder ADAPTED (Phase Z compact / dynamic N rows) :
`compare_table_2col` + NEW parser `compare_row_2col_item` 도입. - figma 원본 1868×1908 px → 1200×590 slide-body scale
- figma 12 hardcoded rows → dynamic N (col_a_body.sections count = 콘텐츠 N)
- figma badge text (axis label) → row 번호 (① ② ③ ...) — 04-2 같은 통합 콘텐츠는
축 라벨 없음, row 번호로 대체. 정책[i] vs 조직[i] 한 row pair.
- figma 결론 영역 (arrow + box) → omit (slide-footer 가 별 area)
3-layer architecture (matrix §0) : slots (payload via process_product_pair builder) :
- V4 = matching authority — V4 ranked this frame use_as_is for 01-2. - title : section.title
- figma_to_html = source/evidence — analysis/texts/index.html/flat/assets full. - col_a_label : 좌 column header (예: "정책 및 발주 체계")
- Phase Z = runtime orchestration — 본 commit 추가 runtime 자원. - col_b_label : 우 column header (예: "조직 및 수행 역량")
- col_a_body.sections[] : 좌 column items, 각 {title, text_lines}
PROMOTED — CSS : - col_b_body.sections[] : 우 column items
- title gradient (#000 → #883700, F13/F14/F12/F11 zone-title family)
- header row : col_a/col_b background tint + bold typography
- row 별 alternating background (zebra) — readability ↑
- cell border : subtle divider (#A5BBB4 family — F11 와 통일)
NOT PROMOTED (P1 case-by-case, compact zone fit) :
- Figma 의 column header 의 *raster icon* (있는 경우) — compact 에서 효과 ↓
- 다양한 색 강조 / 한자 deco (Figma source 보존, 미 promote)
ADAPTED :
- Figma 표 absolute positioning + zoom → Phase Z flex/grid (3-column table)
- typography → token-fixed (zone-title / sub-title / caption / body)
- row 별 height auto (content-fit)
─────────────────────────────────────────────────────────────────────────────
min_height_px derivation :
title 30 + header 30 + 6 data rows × 35 (compact) + padding 30 = 300
+ safety buffer (longer cell content) 50 = **350** (F14 와 동등 class).
Confirm via smoke + R3 artifact + V4 use_as_is target sample (MDX 01-2 미 trigger
in MDX 03 regression, 단 future batch verification 가능).
─────────────────────────────────────────────────────────────────────────────
slots :
- title : section.title
- col_a_label : 좌 column header (예: "BIM")
- col_b_label : 우 column header (예: "DX")
- rows : list[{label, col_a, col_b}] — N category rows
─────────────────────────────────────────────────────────────────────────────
4-class failure taxonomy :
- class 1 adapter readiness : NEW builder `compare_table_2col` + NEW parser
`compare_row_2col_item` 등록 필수 (PAYLOAD_BUILDERS / ITEM_PARSERS). 본 commit.
- class 2 content-fit : row 수 max 12 (builder limit). cell content 한 줄 한계
— 긴 문장 시 truncate 또는 wrap (CSS overflow-hidden).
#} #}
<style> <style>
@@ -57,90 +39,246 @@ slots :
gap: 6px; gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif; font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all; word-break: keep-all;
position: relative;
/* zone aspect 기반 내부 회전 (사용자 lock 2026-05-15) */
container-type: size;
container-name: f18b-root;
} }
/* title — PROMOTED orange-brown gradient (figma line 41-43) */
.f18b__title { .f18b__title {
font-size: var(--font-zone-title); font-size: var(--font-zone-title);
font-weight: 700; font-weight: 700;
line-height: var(--lh-zone-title); line-height: var(--lh-zone-title);
background-image: linear-gradient(180deg, #000 0%, #883700 100%); background-image: linear-gradient(rgb(204,82,0), rgb(136,55,0));
-webkit-background-clip: text; background-clip: text; -webkit-background-clip: text; background-clip: text;
color: transparent; color: transparent;
text-shadow: 0 0 2px rgba(50,44,30,0.3);
flex-shrink: 0; flex-shrink: 0;
filter: drop-shadow(0 0 3px rgba(50,44,30,0.4));
} }
/* compare table — 3-column (category / col_a / col_b) */ /* subheader pill — PROMOTED green-brown gradient (figma line 50) */
.f18b__table { .f18b__subheader {
display: grid; flex-shrink: 0;
grid-template-columns: 1fr 2fr 2fr; height: 30px;
gap: 0; border-radius: 50px;
flex: 1 1 auto; background: linear-gradient(270deg,
min-height: 0; rgb(40,91,74) 0%,
border: 1px solid #A5BBB4; rgba(40,91,74,0.8) 30%,
border-radius: 6px; rgba(74,64,38,0.8) 70%,
overflow: hidden; rgb(74,64,38) 100%);
background: #fff; display: flex;
} align-items: center;
justify-content: space-between;
/* header row */ padding: 0 40px;
.f18b__header {
display: contents;
}
.f18b__header-cell {
background: linear-gradient(180deg, rgba(50,31,9,0.85) 0%, rgba(50,31,9,0.95) 100%);
color: #fff; color: #fff;
font-weight: 700; font-weight: 700;
font-size: var(--font-caption); font-size: var(--font-sub-title);
line-height: 1.2; line-height: 1;
padding: 6px 8px;
text-align: center;
border-bottom: 2px solid #A5BBB4;
}
.f18b__header-cell--category {
background: linear-gradient(180deg, rgba(120,90,40,0.85) 0%, rgba(120,90,40,0.95) 100%);
font-style: italic;
} }
/* data rows */ /* compare rows container — 넓찍 spacing (사용자 lock 2026-05-14). */
.f18b__rows { .f18b__rows {
display: contents; flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
padding: 8px 0;
} }
.f18b__cell {
padding: 5px 8px; /* each row — 3-col flex */
font-size: var(--font-body); .f18b__row {
line-height: var(--lh-body); display: flex;
border-bottom: 1px solid rgba(165,187,180,0.4); align-items: stretch;
color: #1a1a1a; gap: 8px;
overflow: hidden; min-height: 0;
} }
.f18b__cell--category { /* 회전 비활성 — 사용자 lock 2026-05-15 : 03-2 는 2 col compare 유지. */
background: rgba(255,250,240,0.7);
/* col-left : 우측 정렬, brown color (figma line 71-77) */
.f18b__col-left {
flex: 1;
color: #5c3714;
font-weight: 700; font-weight: 700;
color: #5a4b2e; /* PROMOTED — brown family */ text-align: right;
line-height: 1.25;
padding: 4px 8px;
display: flex;
flex-direction: column;
/* 2026-05-15 — center → flex-start. 각 line 위쪽 정렬 (사용자 lock). */
justify-content: flex-start;
}
/* col-right : 좌측 정렬, green color (figma line 89-95) */
.f18b__col-right {
flex: 1;
color: #285b4a;
font-weight: 700;
text-align: left;
line-height: 1.25;
padding: 4px 8px;
display: flex;
flex-direction: column;
/* 2026-05-15 — center → flex-start. 각 line 위쪽 정렬 (사용자 lock). */
justify-content: flex-start;
}
/* col-badge : 가운데 둥근 사각형, green-brown gradient (figma line 78-84) */
.f18b__col-badge {
flex: none;
width: 48px;
border-radius: 10px;
background: linear-gradient(270deg,
rgba(40,91,74,0.8) 0%,
rgba(40,91,74,0.64) 30%,
rgba(74,64,38,0.64) 70%,
rgba(74,64,38,0.8) 100%);
color: #fff;
font-weight: 700;
font-size: var(--font-sub-title);
display: flex;
align-items: center;
justify-content: center;
align-self: stretch;
}
/* item title — bold heading per row */
.f18b__item-title {
font-size: var(--font-body);
font-weight: 900;
margin-bottom: 2px;
}
/* item body lines */
.f18b__item-body {
font-size: var(--font-caption);
line-height: 1.35;
font-weight: 500;
}
.f18b__item-body .text-line {
margin: 0;
}
/* bullets — figma disc inside (line 96-97) */
.f18b__col-left .f18b__item-body .text-line {
list-style-position: inside;
}
.f18b__col-left .f18b__item-body .text-line::before {
content: "• ";
margin-right: 2px;
}
.f18b__col-right .f18b__item-body .text-line::before {
content: "• ";
margin-right: 2px;
}
/* 2026-05-15 — AS-IS/TO-BE transforms mini table (콘텐츠 무손실 + 내용 기반 폭).
사용자 lock : 표 전체는 cell 의 가용 폭 강제 X. 내용에 기반한 width
(display: table = 각 column max content). 표는 우측 정렬 (margin-left: auto).
cell 안 텍스트는 center. 여백 적게. */
.f18b__transforms {
display: table;
margin-left: auto;
margin-right: 0;
border-collapse: separate;
border-spacing: 6px 1px;
font-size: var(--font-caption);
line-height: 1.3;
}
.f18b__transforms-head,
.f18b__transforms-row { display: table-row; }
.f18b__transforms-head > * {
display: table-cell;
text-align: center; text-align: center;
font-weight: 900;
font-size: 0.9em;
color: #6b4423;
padding: 0 4px 1px;
border-bottom: 1px solid rgba(107,68,35,0.3);
} }
/* zebra striping (visual readability) — odd rows alternate */ .f18b__transforms-cell,
.f18b__row:nth-child(odd) .f18b__cell:not(.f18b__cell--category) { .f18b__transforms-arrow {
background: rgba(248,250,252,0.6); display: table-cell;
text-align: center;
padding: 1px 6px;
vertical-align: middle;
} }
.f18b__transforms-cell { color: #1a1a1a; font-weight: 500; }
.f18b__transforms-cell--to { color: #6b4423; font-weight: 700; }
.f18b__transforms-arrow { font-weight: 700; color: #9c6f3f; }
</style> </style>
<div class="f18b" data-frame-id="1171281195" data-template-id="bim_dx_comparison_table"> <div class="f18b" data-frame-id="1171281195" data-template-id="bim_dx_comparison_table">
<div class="f18b__title">{{ slot_payload.title }}</div> <div class="f18b__title">{{ slot_payload.title }}</div>
<div class="f18b__table">
<div class="f18b__header"> {# subheader pill : col_a_label / col_b_label (figma BIM/DX 자리). "vs" 제거. #}
<div class="f18b__header-cell f18b__header-cell--category">구분</div> <div class="f18b__subheader">
<div class="f18b__header-cell">{{ slot_payload.col_a_label | safe }}</div> <span>{{ slot_payload.col_a_label | safe }}</span>
<div class="f18b__header-cell">{{ slot_payload.col_b_label | safe }}</div> <span>{{ slot_payload.col_b_label | safe }}</span>
</div> </div>
<div class="f18b__rows">
{% for row in slot_payload.rows %} {# compare rows : col_a_body / col_b_body 의 sections 를 row 별로 짝지어 표시.
<div class="f18b__row" style="display:contents;"> row 개수 = max(col_a sections, col_b sections). 부족한 쪽은 빈 div.
<div class="f18b__cell f18b__cell--category">{{ row.label | safe }}</div> badge = row 번호 (figma 의 가운데 axis label 자리 — 04-2 같은 통합 콘텐츠는
<div class="f18b__cell f18b__cell--col-a">{{ row.col_a | safe }}</div> 축 라벨 없으므로 번호로 대체). #}
<div class="f18b__cell f18b__cell--col-b">{{ row.col_b | safe }}</div> <div class="f18b__rows">
{% set a_secs = slot_payload.col_a_body.sections or [] %}
{% set b_secs = slot_payload.col_b_body.sections or [] %}
{% set row_count = [a_secs | length, b_secs | length] | max %}
{% for i in range(row_count) %}
{% set a = a_secs[i] if i < (a_secs | length) else none %}
{% set b = b_secs[i] if i < (b_secs | length) else none %}
<div class="f18b__row">
<div class="f18b__col-left" data-frame-slot-id="col_a_body">
{% if a %}
{% if a.title %}<div class="f18b__item-title">{{ a.title | safe }}</div>{% endif %}
<div class="f18b__item-body">
{# 2026-05-15 — 03-2 의 AS-IS/TO-BE markdown table 보존 (mdx 무손실). #}
{% if a.transforms %}
<div class="f18b__transforms">
<div class="f18b__transforms-head">
<span class="f18b__transforms-from">AS-IS</span>
<span></span>
<span class="f18b__transforms-to">TO-BE</span>
</div>
{% for t in a.transforms %}
<div class="f18b__transforms-row">
<div class="f18b__transforms-cell f18b__transforms-cell--from">{{ t.from | safe }}</div>
<div class="f18b__transforms-arrow"></div>
<div class="f18b__transforms-cell f18b__transforms-cell--to">{{ t.to | safe }}</div>
</div>
{% endfor %}
</div>
{% elif a.text_lines %}
{% for line in a.text_lines %}<div class="text-line">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
{% endif %}
</div>
<div class="f18b__col-badge"></div>
<div class="f18b__col-right" data-frame-slot-id="col_b_body">
{% if b %}
{% if b.title %}<div class="f18b__item-title">{{ b.title | safe }}</div>{% endif %}
<div class="f18b__item-body">
{% if b.transforms %}
<div class="f18b__transforms">
<div class="f18b__transforms-head">
<span class="f18b__transforms-from">AS-IS</span>
<span></span>
<span class="f18b__transforms-to">TO-BE</span>
</div>
{% for t in b.transforms %}
<div class="f18b__transforms-row">
<div class="f18b__transforms-cell f18b__transforms-cell--from">{{ t.from | safe }}</div>
<div class="f18b__transforms-arrow"></div>
<div class="f18b__transforms-cell f18b__transforms-cell--to">{{ t.to | safe }}</div>
</div>
{% endfor %}
</div>
{% elif b.text_lines %}
{% for line in b.text_lines %}<div class="text-line">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
{% endif %}
</div>
</div> </div>
{% endfor %} {% endfor %}
</div>
</div> </div>
</div> </div>
@@ -5,50 +5,50 @@
Visual Provenance — figma_to_html_agent/blocks/1171281198/ (frame 20) Visual Provenance — figma_to_html_agent/blocks/1171281198/ (frame 20)
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
Frame 20 = "디지털 전환(DX)은 S/W가 필수다" (cards-3-header). Figma 원본 = Frame 20 = "디지털 전환(DX)은 S/W가 필수다" (cards-3-header). Figma 원본 =
상단 dark green banner ("디지털 전환(DX)은 S/W가 필수다" green/white mix) + 상단 dark banner ("디지털 전환(DX)은 S/W가 필수다") + 좌측 DX circular area
좌측 DX circular area + 우측 3 perspective cards (BIM 전면설계 / 디지털 + 우측 3 perspective cards (BIM 전면설계 / 디지털 전환 S/W / 고부가가치 산업전환).
전환 S/W / 고부가가치 산업전환).
본 partial = Track A frame 5 (Codex round 45 accepted, V4 LE tier strongest). 본 partial = Figma 1:1 — *우측 3 cards* 만 가져옴.
builder 재사용 = `quadrant_flat_slots` (F11 cards-3-category pattern). 새 좌측 DX circle area = 사용자 지시로 제외 (zone fit). 중목차 (zone title)
builder/parser 0. 디자인 = 건드리지 않음. Figma asset PNG 직접 사용 (recolor X) :
card_frame.png (515.579×377.767) — 카드 외곽 stroke/frame
card_header_bg.png (378.435×85.881) — 카드 header 배경
card_bottom_decor.png(515.579×185.746) — 카드 하단 장식
bullet_arrow.png (41.246×39.486) — ▶ 화살표
3-layer architecture (matrix §0) : PROMOTED — Figma visual identity 1:1 :
- V4 = matching authority — V4 ranked this frame light_edit for 03-1 + 01-1. - 카드 frame asset (외곽 line + 내부 background)
- figma_to_html (1171281198) = source/evidence — 386-line index.html + assets/. - header bg asset (상단 colored bar)
- Phase Z = runtime — 본 commit adds catalog + partial + smoke fixture. - bottom decor asset (하단 그림자 + 장식)
- 카드 title = white fill + 컬러 stroke + dark glow
· card 1 stroke = #1d4d3e (dark green)
· card 2 stroke = #cc5200 (orange)
· card 3 stroke = #1d4d3e (dark green)
· text-shadow = 0 0 4px #322c1e (모든 card 공통)
· paint-order = stroke fill (Figma 렌더 일치)
- bullet = bullet_arrow.png (CSS recolor X)
PROMOTED — CSS : NOT PROMOTED (zone fit / 사용자 지시) :
- 3 column header bg : dark green (`#296B55` family, Figma green theme) - 좌측 DX circular area (multi-image + center text + decor)
- header text white bold + green accent - 상단 dark banner (zone title 이 대체)
- title gradient (#000 → #883700, F13/F14/F12/F11/F18 zone-title family) - hanmaek-label / left-bg / left-decor / left-inner-bg / middle-overlay
- card border + bullet markers (green family)
NOT PROMOTED (P1 case-by-case, compact zone fit) : ADAPTED — geometry only :
- 상단 dark green banner (Figma 의 큰 visual 영역, MDX 의 *title 만* 핵심) - Figma 1682×540 px 3-card canvas → zone-compatible 3-col grid
- 좌측 DX circular area (multi-image + center text + decor — Figma 의 main - 카드 내부 absolute 좌표 → flex column (bullet 개수 가변 대응)
rhetorical anchor 단 compact zone 에서 표현 불가). title 의 gradient 로 의도 보존 - Figma 40px title / 37px body → token-fixed (sub-title / body)
- 한자/장식/배경 텍스처 — Figma source evidence 보존, 미 promote
ADAPTED : assets_dir : `assets/<template_id>` (run-relative). render() 호출 시 주입.
- Figma 90/65/40px → token-fixed (zone-title 13 / sub-title 12 / caption / body 11)
- 1280×426 absolute positioning + zoom → Phase Z 3-column grid
- 3 perspective cards (header + body) → flex column
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
min_height_px derivation : min_height_px derivation :
3 col × (header 30 + body 3-bullets × 25) + title 30 + padding 30 = ~195 3 col × (header 50 + body 3-bullets × 26) + decor 60 + title 30 + padding 20
+ safety buffer (longer label / 5+ bullets) 125 = **320**. F12/F11/F18 class. = ~238 + buffer 82 = **320**. F12/F11/F18 class.
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
slots : slots :
- title : section.title - title : section.title
- perspective_1/2/3_label : 3 column headers - perspective_1/2/3_label : 3 card headers
- perspective_1/2/3_body : list[{text, indent}] : 3 column bullets - perspective_1/2/3_body : list[{text, indent}] : 3 card bullets
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
4-class failure taxonomy :
- class 1 readiness : builder reuse `quadrant_flat_slots` + `quadrant_item`.
contract + partial + smoke fixture.
- class 2 content-fit : header label 짧게 (single line). body 3-5 bullets per
column (zone fit). 6+ bullets 시 watch.
#} #}
<style> <style>
@@ -58,7 +58,13 @@ slots :
gap: 6px; gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif; font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all; word-break: keep-all;
/* Axis : zone aspect-ratio 에 따라 내부 layout 자동 회전 (사용자 lock 2026-05-15).
horizontal-2 zone (wide ~ 4.27 aspect) → 3 cards horizontal.
vertical-2 zone (~1 aspect) → 3 cards vertical. container query trigger. */
container-type: size;
container-name: f20b-root;
} }
/* 중목차 (zone title) — Figma title gradient 보존, 사용자 지시로 미변경 */
.f20b__title { .f20b__title {
font-size: var(--font-zone-title); font-size: var(--font-zone-title);
font-weight: 700; font-weight: 700;
@@ -70,84 +76,223 @@ slots :
filter: drop-shadow(0 0 3px rgba(50,44,30,0.4)); filter: drop-shadow(0 0 3px rgba(50,44,30,0.4));
} }
/* 3-column grid — Figma cards-3-header pattern */ /* 3-card grid — Figma 우측 3 카드 영역 1:1.
.f20b__cols { ::before 로 figma body_bg.png (hazy sky/cloud) + middle_overlay.png
(peach gradient) 두 layer 를 카드 영역 외곽에 깔아 figma 원본 분위기 재현.
zone title 은 이 영역 밖이라 영향 없음 (사용자 룰: 중목차 디자인 미변경). */
.f20b__cards {
position: relative;
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 10px; grid-template-rows: auto;
gap: 14px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
} padding: 10px 8px 12px;
.f20b__col { border-radius: 12px;
display: flex; flex-direction: column;
border: 2px solid #296B55; /* PROMOTED — green family from Figma */
border-radius: 8px;
overflow: hidden; overflow: hidden;
background: #fff; }
min-height: 0; /* zone aspect < 1.5 → 3 cards vertical stack. auto rows + align-content: start
= 콘텐츠 height 만큼, 위쪽 정렬 (좌/우 zone visual balance). */
@container f20b-root (aspect-ratio < 1.5) {
.f20b__cards {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
grid-auto-flow: row;
align-content: start;
}
}
.f20b__cards::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--frame-middle-overlay), var(--frame-body-bg);
background-size: 100% 100%, 100% 100%;
background-repeat: no-repeat;
background-position: center, center;
opacity: 0.9;
z-index: 0;
pointer-events: none;
} }
/* header bar (top of each card, dark green per Figma) */ /* 개별 카드 — figma card_frame.png 가 외곽 stroke 담당.
z-index 1 = ::before (body_bg + middle_overlay) 위에 카드가 떠 있게.
Axis B fit (2026-05-14) — container-type: size 로 카드 body 안의 line-height
/ padding / gap 이 카드 height + 콘텐츠 line 수에 *수학적 비례* (cqh 단위).
zone 작아지면 자동 축소, 커지면 자동 확대. */
.f20b__card {
position: relative;
z-index: 1;
display: flex; flex-direction: column;
min-height: 0;
padding: 0 0 10px 0;
container-type: size;
}
/* card_frame.png — 외곽 line + 내부 light fill (figma 1:1) */
.f20b__card::before {
content: "";
position: absolute;
inset: 0 0 12px 0;
background-image: var(--card-frame-bg);
background-size: 100% 100%;
background-repeat: no-repeat;
z-index: 0;
pointer-events: none;
}
/* card_bottom_decor.png — 하단 장식. Axis B = 30% → 20% 로 body 공간 확보. */
.f20b__card::after {
content: "";
position: absolute;
left: 0; right: 0;
bottom: 0;
height: 22%;
background-image: var(--card-decor-bg);
background-size: 100% 100%;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
/* card header — figma card_header_bg.png 배경 + white 카드 title. Axis B tight. */
.f20b__header { .f20b__header {
background: linear-gradient(180deg, #296B55 0%, #123328 100%); /* PROMOTED — Figma green theme */ position: relative;
color: #fff; z-index: 2;
margin: 4px 8px 0 8px;
padding: 3px 6px;
min-height: 28px;
display: flex; align-items: center; justify-content: center;
background-image: var(--card-header-bg);
background-size: 100% 100%;
background-repeat: no-repeat;
flex-shrink: 0;
}
.f20b__header-text {
font-weight: 700; font-weight: 700;
font-size: var(--font-sub-title); font-size: var(--font-sub-title);
line-height: 1.15; line-height: 1.15;
padding: 6px 10px;
text-align: center; text-align: center;
flex-shrink: 0; color: #ffffff;
/* figma 카드 title — white fill + 컬러 stroke + dark glow, paint-order stroke fill */
-webkit-text-stroke: 1px var(--card-title-stroke);
text-shadow: 0 0 3px #322c1e;
paint-order: stroke fill;
letter-spacing: -0.04em; letter-spacing: -0.04em;
} }
/* body — bullet list (CSS check marker, F14 pattern) */ /* card 1 / 3 = dark green stroke, card 2 = orange stroke (figma 1:1) */
.f20b__card--1 { --card-title-stroke: #1d4d3e; }
.f20b__card--2 { --card-title-stroke: #cc5200; }
.f20b__card--3 { --card-title-stroke: #1d4d3e; }
/* body — bullet list with bullet_arrow.png (figma asset).
figma 원본 본문 weight=500 (Medium) 인데 zone 크기에서는 header (700+stroke+glow)
와 시각적 차이가 작아 본문도 강조처럼 보임. font-size 룰 지켜야 하므로
weight 400 + 부드러운 color (#333) 로 명도 차이 확보. header = 무게+stroke,
body = 가볍고 어두운 검정. */
.f20b__body { .f20b__body {
position: relative;
z-index: 2;
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
padding: 6px 10px 8px; /* Axis B 비례 fit — padding/gap 도 카드 height 비례 (cqh).
color: #1a1a1a; 카드 작을수록 자동 tight, 클수록 자동 여유. min/max clamp 로 극단 방지. */
padding: clamp(2px, 2cqh, 8px) clamp(6px, 3cqw, 12px) clamp(1px, 1cqh, 4px);
color: #333;
min-height: 0; min-height: 0;
display: flex; flex-direction: column; display: flex; flex-direction: column;
gap: 3px; gap: clamp(0px, 0.6cqh, 4px);
} }
.f20b__body .text-line { .f20b__body .text-line {
color: inherit; color: inherit;
font-size: var(--font-body); font-size: var(--font-body);
line-height: var(--lh-body); /* line-height 핵심 산식 (사용자 lock 2026-05-14) :
카드 body 의 가용 height (~70cqh, header 28cqh / decor 22cqh 제외) 를
line count (--max-body-lines, jinja 에서 카드별 max bullet 수 주입) 로 나눠
per-line height. clamp 로 minimum (글자 겹침 방지) + maximum (너무 spaced
방지). font-size 미변경 (사용자 룰). */
line-height: clamp(
1.05em,
calc(70cqh / var(--max-body-lines, 5) - 0px),
1.5em
);
font-weight: 400;
letter-spacing: -0.02em;
position: relative; position: relative;
padding-left: 14px; padding-left: 14px;
min-height: 0;
} }
.f20b__body .text-line--bullet::before { .f20b__body .text-line--bullet::before {
content: "\2713"; /* ✓ check mark — green theme */ content: "";
position: absolute; position: absolute;
left: 0; top: 0; left: 0; top: 0.3em;
color: #296B55; /* PROMOTED — green family */ width: 10px; height: 9px;
font-weight: 700; background-image: var(--bullet-arrow);
background-size: contain;
background-repeat: no-repeat;
background-position: left center;
} }
/* indent level (Phase Z mapper 가 indent 1+ 의 텍스트 line 을 시각적 sub-bullet
으로 표시할 때 사용). Axis B fit — padding-left 32 → 20 으로 zone 가용 폭 확보. */
.f20b__body .text-line--indent-1 { padding-left: 20px; color: #555; }
.f20b__body .text-line--indent-1::before { left: 8px; opacity: 0.65; }
</style> </style>
<div class="f20b" data-frame-id="1171281198" data-template-id="dx_sw_necessity_three_perspectives"> {# assets_dir = 'assets/<template_id>' (run-relative). pipeline render() 가 주입.
url() 안의 single-quote 는 jinja autoescape (style= attribute) 가 &#39; 로 바꿔
CSS parse 가 실패. quote 빼고 (asset path 에 space/특수문자 없음) + `| safe`. #}
{% set _assets = assets_dir | default('', true) %}
{% set _frame_bg = "url(" ~ _assets ~ "/card_frame.png)" if _assets else 'none' %}
{% set _header_bg = "url(" ~ _assets ~ "/card_header_bg.png)" if _assets else 'none' %}
{% set _decor_bg = "url(" ~ _assets ~ "/card_bottom_decor.png)" if _assets else 'none' %}
{% set _bullet_bg = "url(" ~ _assets ~ "/bullet_arrow.png)" if _assets else 'none' %}
{% set _body_bg = "url(" ~ _assets ~ "/body_bg.png)" if _assets else 'none' %}
{% set _middle_ov = "url(" ~ _assets ~ "/middle_overlay.png)" if _assets else 'none' %}
<div class="f20b"
data-frame-id="1171281198"
data-template-id="dx_sw_necessity_three_perspectives"
style="--card-frame-bg: {{ _frame_bg | safe }};
--card-header-bg: {{ _header_bg | safe }};
--card-decor-bg: {{ _decor_bg | safe }};
--bullet-arrow: {{ _bullet_bg | safe }};
--frame-body-bg: {{ _body_bg | safe }};
--frame-middle-overlay: {{ _middle_ov | safe }};">
<div class="f20b__title">{{ slot_payload.title }}</div> <div class="f20b__title">{{ slot_payload.title }}</div>
<div class="f20b__cols">
{# 3 columns — quadrant_flat_slots produces perspective_N_label / perspective_N_body for N=1..3 #} {# Axis B 산식 — 카드별 bullet line 수 = body lines. card --max-body-lines 로
<div class="f20b__col"> line-height clamp 계산에 사용. perspective_N_body 가 list 또는 None. #}
<div class="f20b__header">{{ slot_payload.perspective_1_label | safe }}</div> {% set p1_lines = (slot_payload.perspective_1_body | length) if slot_payload.perspective_1_body else 1 %}
{% set p2_lines = (slot_payload.perspective_2_body | length) if slot_payload.perspective_2_body else 1 %}
{% set p3_lines = (slot_payload.perspective_3_body | length) if slot_payload.perspective_3_body else 1 %}
<div class="f20b__cards">
{# 3 cards — quadrant_flat_slots produces perspective_N_label / perspective_N_body for N=1..3.
각 카드 inline --max-body-lines : 카드의 실제 line 수 → line-height 비례 산식. #}
<div class="f20b__card f20b__card--1" style="--max-body-lines: {{ p1_lines }};">
<div class="f20b__header">
<span class="f20b__header-text">{{ slot_payload.perspective_1_label | safe }}</span>
</div>
<div class="f20b__body"> <div class="f20b__body">
{% if slot_payload.perspective_1_body %} {% if slot_payload.perspective_1_body %}
{% for line in slot_payload.perspective_1_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.perspective_1_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="f20b__col"> <div class="f20b__card f20b__card--2" style="--max-body-lines: {{ p2_lines }};">
<div class="f20b__header">{{ slot_payload.perspective_2_label | safe }}</div> <div class="f20b__header">
<span class="f20b__header-text">{{ slot_payload.perspective_2_label | safe }}</span>
</div>
<div class="f20b__body"> <div class="f20b__body">
{% if slot_payload.perspective_2_body %} {% if slot_payload.perspective_2_body %}
{% for line in slot_payload.perspective_2_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.perspective_2_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="f20b__col"> <div class="f20b__card f20b__card--3" style="--max-body-lines: {{ p3_lines }};">
<div class="f20b__header">{{ slot_payload.perspective_3_label | safe }}</div> <div class="f20b__header">
<span class="f20b__header-text">{{ slot_payload.perspective_3_label | safe }}</span>
</div>
<div class="f20b__body"> <div class="f20b__body">
{% if slot_payload.perspective_3_body %} {% if slot_payload.perspective_3_body %}
{% for line in slot_payload.perspective_3_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.perspective_3_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
@@ -40,6 +40,9 @@ slots: title (omitted), banner_left, banner_right,
font-family: 'Noto Sans KR', 'Pretendard', sans-serif; font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all; word-break: keep-all;
overflow: hidden; overflow: hidden;
/* zone aspect 기반 내부 회전 (사용자 lock 2026-05-15) */
container-type: size;
container-name: f29b-root;
} }
.f29b__title { .f29b__title {
font-size: var(--font-zone-title); /* 13px — frame 13 의 f13b__title 과 동일 위계 */ font-size: var(--font-zone-title); /* 13px — frame 13 의 f13b__title 과 동일 위계 */
@@ -119,37 +122,46 @@ slots: title (omitted), banner_left, banner_right,
linear-gradient(to bottom, rgba(41,107,85,0.3) 0%, rgba(255,255,255,0.3) 100%) 100% 0% / 50% 100% no-repeat; linear-gradient(to bottom, rgba(41,107,85,0.3) 0%, rgba(255,255,255,0.3) 100%) 100% 0% / 50% 100% no-repeat;
} }
/* 6 cell grid: 3 rows (section 1/2/3), 2 cols (process/product). 각 row 의 height = max(좌, 우) */ /* 6 cell grid: 3 rows (section 1/2/3), 2 cols (process/product).
Axis B (zone-fit, 2026-05-14) — minmax(0,1fr) 실험 결과 균등 분할로 *역효과*
(짧은 row 가 공간 낭비, 긴 row 가 더 짤림). grid-auto-rows: auto + grid
container overflow: hidden + padding/gap tight. font-size 미변경. */
.f29b__grid { .f29b__grid {
position: relative; position: relative;
z-index: 1; z-index: 1;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-auto-rows: auto; grid-auto-rows: auto;
gap: 6px 10px; gap: 4px 8px;
padding: 4px 8px; padding: 3px 6px;
height: 100%; height: 100%;
align-content: start; min-height: 0;
overflow: hidden;
} }
/* 회전 비활성 — 사용자 lock 2026-05-15 : 03-2 는 기존 2 col layout 이 더 자연스러움.
(회전은 03-1 의 3 pillars / 3 cards 만 의도). */
.f29b__cell { .f29b__cell {
overflow: hidden; overflow: hidden;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
display: flex;
flex-direction: column;
} }
/* row 2/3 (3 번째 cell 이후) 위에 dashed divider — 좌우 같은 Y */ /* row 2/3 (3 번째 cell 이후) 위에 dashed divider — 좌우 같은 Y. fit 정비로 tight. */
.f29b__cell:nth-child(n+3) { .f29b__cell:nth-child(n+3) {
border-top: 1px dashed rgba(0,0,0,0.15); border-top: 1px dashed rgba(0,0,0,0.15);
padding-top: 6px; padding-top: 3px;
margin-top: 4px; margin-top: 2px;
} }
/* section-title — 부소목차 (사용자 5-layer 직설 2026-05-07): Analogue 기반... 같은 깊이 0 굵은 bullet */ /* section-title — 부소목차 (사용자 5-layer 직설 2026-05-07): Analogue 기반... 같은 깊이 0 굵은 bullet */
.f29b__section-title { .f29b__section-title {
font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */ font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */
font-weight: 900; font-weight: 900;
line-height: 1.3; line-height: 1.25;
margin-bottom: 5px; margin-bottom: 3px;
padding-left: 6px; padding-left: 6px;
border-left: 3px solid currentColor; border-left: 3px solid currentColor;
flex-shrink: 0;
} }
.f29b__cell--left .f29b__section-title { color: #5C3714; } /* PROMOTED line 125 */ .f29b__cell--left .f29b__section-title { color: #5C3714; } /* PROMOTED line 125 */
.f29b__cell--right .f29b__section-title { color: #084C56; } /* PROMOTED line 126 */ .f29b__cell--right .f29b__section-title { color: #084C56; } /* PROMOTED line 126 */
@@ -157,9 +169,28 @@ slots: title (omitted), banner_left, banner_right,
font-size: var(--font-caption); /* 10px */ font-size: var(--font-caption); /* 10px */
margin-left: 3px; margin-left: 3px;
} }
/* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override. */ /* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override + Axis B
local line-height (token --lh-body 보다 살짝 tight). font-size 미변경 (사용자 룰).
공통 여백 token 은 안 건드리고 frame 내부 local override 만. */
.f29b__cell .text-line {
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.f29b__cell .text-line + .text-line { margin-top: 1px; }
.f29b__cell .text-line--bullet { color: #000; font-weight: 700; } .f29b__cell .text-line--bullet { color: #000; font-weight: 700; }
/* popup link — 각 column 끝, 파란색 글씨 (사용자 lock 2026-05-14). */
.f29b__popup-link {
margin-top: 2px;
font-size: var(--font-caption);
font-weight: 600;
color: #1a73e8;
cursor: pointer;
text-decoration: underline;
}
.f29b__popup-link::before { content: "▶ "; font-size: 0.85em; }
/* AS-IS/TO-BE 좌우 + arrow (PROMOTED 구조) */ /* AS-IS/TO-BE 좌우 + arrow (PROMOTED 구조) */
.f29b__transform { .f29b__transform {
display: grid; display: grid;
@@ -215,80 +246,71 @@ slots: title (omitted), banner_left, banner_right,
<div class="f29b__grid"> <div class="f29b__grid">
{# ───── Row 1: section 1 ───── #} {# 2026-05-14 — dynamic cardinality loop. row 0..N-1, N = max(process.sections, product.sections).
{% set s1 = slot_payload.process.sections[0] %} 원본 콘텐츠 N cards 가 어떤 값이든 (3 / 4 / 5 ...) 그대로 표시. 부족한 쪽은 빈 cell.
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column"> row 0 의 process column 만 AS-IS/TO-BE transform block 지원 (legacy 03 호환). #}
{% if s1.title %} {% set proc_len = slot_payload.process.sections | length %}
<div class="f29b__section-title">{{ s1.title | safe }}</div> {% set prod_len = slot_payload.product.sections | length %}
{% if s1.transforms %} {% set row_count = [proc_len, prod_len] | max %}
<div class="transform-block"> {% for r in range(row_count) %}
<div class="transform-block__header"> {% set s = slot_payload.process.sections[r] if r < proc_len else none %}
<span class="transform-block__label--from">AS-IS</span> {% set p = slot_payload.product.sections[r] if r < prod_len else none %}
<span></span>
<span class="transform-block__label--to">TO-BE</span> <div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
</div> {% if s and s.title %}
<div class="transform-rows"> <div class="f29b__section-title">{{ s.title | safe }}</div>
{% for t in s1.transforms %} {% if s.transforms %}
<div class="transform-row"> <div class="transform-block">
<div class="transform-row__from">{{ t.from | safe }}</div> <div class="transform-block__header">
<div class="transform-row__arrow"></div> <span class="transform-block__label--from">AS-IS</span>
<div class="transform-row__to">{{ t.to | safe }}</div> <span></span>
<span class="transform-block__label--to">TO-BE</span>
</div>
<div class="transform-rows">
{% for t in s.transforms %}
<div class="transform-row">
<div class="transform-row__from">{{ t.from | safe }}</div>
<div class="transform-row__arrow"></div>
<div class="transform-row__to">{{ t.to | safe }}</div>
</div>
{% endfor %}
</div> </div>
{% endfor %}
</div> </div>
{% elif s.text_lines %}
{% for line in s.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
{% endif %}
</div> </div>
{% elif s1.text_lines %}
{% for line in s1.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
{% endif %}
</div>
{% set p1 = slot_payload.product.sections[0] %} <div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column"> {% if p and p.title %}
{% if p1.title %} <div class="f29b__section-title">{{ p.title | safe }}{% if p.sub_title %}<span class="f29b__section-title__sub">{{ p.sub_title | safe }}</span>{% endif %}</div>
<div class="f29b__section-title">{{ p1.title | safe }}</div> {% for line in p.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% for line in p1.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% if p.footnote %}
{% endif %} <div class="f29b__footnote"><span class="f29b__aster-small"></span> {{ p.footnote | safe }}</div>
</div> {% endif %}
{% endif %}
{# ───── Row 2: section 2 ───── #} </div>
{% set s2 = slot_payload.process.sections[1] %} {% endfor %}
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
{% if s2.title %}
<div class="f29b__section-title">{{ s2.title | safe }}</div>
{% for line in s2.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
{% set p2 = slot_payload.product.sections[1] %}
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
{% if p2.title %}
<div class="f29b__section-title">{{ p2.title | safe }}</div>
{% for line in p2.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% if p2.footnote %}
<div class="f29b__footnote"><span class="f29b__aster-small"></span> {{ p2.footnote | safe }}</div>
{% endif %}
{% endif %}
</div>
{# ───── Row 3: section 3 ───── #}
{% set s3 = slot_payload.process.sections[2] %}
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
{% if s3.title %}
<div class="f29b__section-title">{{ s3.title | safe }}</div>
{% for line in s3.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
{% set p3 = slot_payload.product.sections[2] %}
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
{% if p3.title %}
<div class="f29b__section-title">{{ p3.title | safe }}{% if p3.sub_title %}<span class="f29b__section-title__sub">{{ p3.sub_title | safe }}</span>{% endif %}</div>
{% for line in p3.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div> </div>
{# 2026-05-14 — popup links : 좌/우 column 끝에 파란색 글씨 (사용자 lock).
slot_payload.popup_left_summary / popup_right_summary 가 있으면 표시. #}
{% if slot_payload.popup_left_summary or slot_payload.popup_right_summary %}
<div class="f29b__popups" style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; padding: 0 8px;">
<div>
{% if slot_payload.popup_left_summary %}
<div class="f29b__popup-link">{{ slot_payload.popup_left_summary | safe }}</div>
{% endif %}
</div>
<div>
{% if slot_payload.popup_right_summary %}
<div class="f29b__popup-link">{{ slot_payload.popup_right_summary | safe }}</div>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
@@ -4,34 +4,38 @@
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
Visual Provenance — figma_to_html_agent/blocks/1171281209/ (frame 28) Visual Provenance — figma_to_html_agent/blocks/1171281209/ (frame 28)
───────────────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────────────
Frame 28 = "현존 상용 S/W의 현실" (cards/title-plus-3-emphasis). 3 emphasis Frame 28 = "현존 상용 S/W의 현실".
cards parallel — 3 problem/약점/한계 진단 (토목 전문성 부족 / 비효율성 / 사용자 lock (2026-05-14) : figma 의 1 frame visual (외곽 빨간 border + pill +
실무 적용 불가). 본문) 을 *3 개 두루마리로 복제*, horizontal 3-column.
- 각 두루마리 box : 외곽 빨간 border (#9C0E0E) + radius
- 상단 pill (title_pill.png) : emphasis_N_label (예 : "토목 전문성 부족")
- 본문 paragraph : emphasis_N_body 의 text-line 들 (▶ bullet + .accent)
본 partial = Track A frame 7 (V4-zero catalog-completeness activation per slots :
Codex round 49 guardrail). V4 = matching authority 보존. Phase Z 는 runtime - title : zone 중목차 (별도 위 영역)
availability 만 추가. - emphasis_1/2/3_label : 각 두루마리 pill 안
- emphasis_1/2/3_body : list[{text}] : 각 두루마리 본문
PROMOTED CSS : Axis B 수학 fit (cqh + jinja line count) :
- per-emphasis 경고 색 (red/orange/amber — problem theme) - border / pill / padding / bullet size = clamp(min, cqh/cqw 비례, max)
- title gradient (zone-title family) - line-height = clamp(1.1em, calc(가용 cqh / N), 1.5em),
- bullet markers per-emphasis color N = jinja 가 emphasis_N_body | length 로 계산해 inline --max-body-lines 주입
- font-size 미변경 (사용자 룰)
NOT PROMOTED : Figma source decoration / banner / texture — 보존 (figma_to_html). ─────────────────────────────────────────────────────────────────────────────
ADAPTED : Figma absolute → flex 3-column grid + token-fixed typography.
slots : title, emphasis_1/2/3_label, emphasis_1/2/3_body.
#} #}
<style> <style>
.f28b { .f28b {
width: 100%; height: 100%; width: 100%; height: 100%;
display: flex; flex-direction: column; display: flex; flex-direction: column;
gap: 6px; gap: 4px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif; font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all; word-break: keep-all;
/* zone aspect 기반 내부 회전 (사용자 lock) */
container-type: size;
container-name: f28b-root;
} }
/* 중목차 (zone title) — gradient 보존, 사용자 룰 (안 건드림). */
.f28b__title { .f28b__title {
font-size: var(--font-zone-title); font-size: var(--font-zone-title);
font-weight: 700; font-weight: 700;
@@ -43,91 +47,195 @@ slots : title, emphasis_1/2/3_label, emphasis_1/2/3_body.
filter: drop-shadow(0 0 3px rgba(50,44,30,0.4)); filter: drop-shadow(0 0 3px rgba(50,44,30,0.4));
} }
.f28b__cols { /* ── 3 두루마리 grid — zone aspect 에 따라 horizontal/vertical 회전 ── */
.f28b__cards {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 10px; grid-template-rows: auto;
gap: clamp(6px, 1.5cqw, 14px);
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
padding-top: clamp(6px, 3cqh, 12px);
} }
.f28b__col { /* zone aspect < 1.5 → 3 두루마리 vertical stack. auto rows + align-content: start
display: flex; flex-direction: column; = 콘텐츠 height 만큼, 위쪽 정렬 (좌/우 zone visual balance). */
border: 2px solid; @container f28b-root (aspect-ratio < 1.5) {
border-radius: 8px; .f28b__cards {
overflow: hidden; grid-template-columns: 1fr;
background: #fff; grid-template-rows: auto auto auto;
min-height: 0; grid-auto-flow: row;
align-content: start;
}
} }
/* per-emphasis problem theme — orange/red family for 약점/한계 진단 */
.f28b__col:nth-child(1) { border-color: #dc2626; } /* red */
.f28b__col:nth-child(2) { border-color: #ea580c; } /* orange */
.f28b__col:nth-child(3) { border-color: #d97706; } /* amber */
.f28b__header { /* 두루마리 박스 — figma 1 frame 의 visual 정체성 1:1. 사용자 lock 2026-05-14
color: #fff; "두루마리 좀 더 크게" → padding-top 줄여 body 영역 확장. */
font-weight: 700; .f28b__card {
position: relative;
display: flex; flex-direction: column;
min-height: 0;
border: clamp(3px, 1.2cqh, 6px) solid #9C0E0E;
border-radius: clamp(6px, 1.5cqh, 12px);
background: linear-gradient(to bottom right, #fff 0%, #FBEDE8 100%);
container-type: size;
overflow: visible;
padding-top: clamp(10px, 5cqh, 16px);
}
/* 상단 pill — emphasis label 텍스트가 figma 의 red→darkred gradient + glow.
title_pill.png asset 이 있으면 배경, 없으면 빨간 fallback. */
.f28b__pill {
position: absolute;
top: clamp(-12px, -3cqh, -4px);
left: 50%;
transform: translateX(-50%);
min-width: 60%;
max-width: 90%;
height: clamp(14px, 11cqh, 26px);
background-image: var(--pill-bg);
background-size: 100% 100%;
background-repeat: no-repeat;
background-color: #9C0E0E;
border-radius: 9999px;
z-index: 3;
display: flex; align-items: center; justify-content: center;
padding: 0 clamp(6px, 2cqw, 12px);
box-shadow: 0 2px 4px rgba(0,0,0,0.18);
}
.f28b__pill-text {
font-weight: 900;
font-size: var(--font-sub-title); font-size: var(--font-sub-title);
line-height: 1.15; line-height: 1;
padding: 6px 10px; color: #fff;
text-align: center; letter-spacing: -0.03em;
flex-shrink: 0; white-space: nowrap;
letter-spacing: -0.02em;
}
.f28b__col:nth-child(1) .f28b__header { background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%); }
.f28b__col:nth-child(2) .f28b__header { background: linear-gradient(180deg, #ea580c 0%, #9a3412 100%); }
.f28b__col:nth-child(3) .f28b__header { background: linear-gradient(180deg, #d97706 0%, #92400e 100%); }
.f28b__body {
flex: 1 1 auto;
overflow: hidden; overflow: hidden;
padding: 6px 10px 8px; text-overflow: ellipsis;
color: #1a1a1a; max-width: 100%;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
/* 두루마리 본문 — line 별 indent 구조 (indent 0 = 소소목차 ✓ / indent 1 = 서브 ▶) */
.f28b__body {
position: relative;
flex: 1 1 auto;
min-height: 0; min-height: 0;
display: flex; flex-direction: column; overflow: hidden;
gap: 3px; display: flex;
flex-direction: column;
gap: clamp(2px, 1cqh, 6px);
padding: clamp(4px, 2cqh, 10px) clamp(6px, 2cqw, 14px) clamp(4px, 2cqh, 10px);
/* line-height 산식 — body 가용 cqh ≈ 75. N = jinja 가 line 수 주입. */
line-height: clamp(
1.2em,
calc(75cqh / var(--max-body-lines, 4)),
1.7em
);
font-size: var(--font-body);
color: #1a1a1a;
} }
.f28b__body .text-line { .f28b__body .text-line {
color: inherit;
font-size: var(--font-body);
line-height: var(--lh-body);
position: relative; position: relative;
padding-left: 14px; padding-left: clamp(14px, 4cqw, 22px);
line-height: inherit;
} }
.f28b__body .text-line--bullet::before { /* indent 0 — 소소목차 볼드체 (mdx 의 <strong>) 앞에 ✓ 빨간 마크 (사용자 lock) */
content: "!"; .f28b__body .text-line--heading {
position: absolute;
left: 4px; top: 0;
font-weight: 700; font-weight: 700;
color: #1a1a1a;
margin-top: clamp(1px, 0.5cqh, 4px);
} }
.f28b__col:nth-child(1) .f28b__body .text-line--bullet::before { color: #dc2626; } .f28b__body .text-line--heading:first-child { margin-top: 0; }
.f28b__col:nth-child(2) .f28b__body .text-line--bullet::before { color: #ea580c; } .f28b__body .text-line--heading::before {
.f28b__col:nth-child(3) .f28b__body .text-line--bullet::before { color: #d97706; } content: "\2713"; /* ✓ */
position: absolute;
left: clamp(0px, 0.5cqw, 4px);
top: 0;
font-weight: 900;
color: #9C0E0E;
line-height: inherit;
}
/* indent 1+ — 서브 detail 본문 앞에 ▶ bullet_arrow.png (figma asset). 들여쓰기. */
.f28b__body .text-line--sub {
padding-left: clamp(22px, 6cqw, 32px);
color: #444;
font-weight: 400;
}
.f28b__body .text-line--sub::before {
content: "";
position: absolute;
left: clamp(10px, 3cqw, 16px);
top: 0.45em;
width: clamp(7px, 2cqw, 11px);
height: clamp(6px, 1.8cqw, 10px);
background-image: var(--bullet-arrow);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
/* asset 없을 때 fallback — 작은 ▶ 글자 */
.f28b--no-assets .f28b__body .text-line--sub::before {
content: "\25B8"; /* ▸ */
background: none;
width: auto;
height: auto;
top: 0;
color: #9C0E0E;
font-weight: 700;
line-height: inherit;
}
/* mdx text 안 <strong> 보존 — accent 강조는 .accent 클래스도 인식 */
.f28b__body .text-line strong { color: #1a1a1a; }
.f28b__body .text-line--heading strong { color: inherit; }
.f28b__body .text-line .accent { color: #CC5200; font-weight: 700; }
</style> </style>
<div class="f28b" data-frame-id="1171281209" data-template-id="sw_reality_three_emphasis"> {% set _assets = assets_dir | default('', true) %}
{% set _bullet_bg = "url(" ~ _assets ~ "/bullet_arrow.png)" if _assets else 'none' %}
{% set _pill_bg = "url(" ~ _assets ~ "/title_pill.png)" if _assets else 'none' %}
{% set e1_lines = (slot_payload.emphasis_1_body | length) if slot_payload.emphasis_1_body else 1 %}
{% set e2_lines = (slot_payload.emphasis_2_body | length) if slot_payload.emphasis_2_body else 1 %}
{% set e3_lines = (slot_payload.emphasis_3_body | length) if slot_payload.emphasis_3_body else 1 %}
<div class="f28b{% if not _assets %} f28b--no-assets{% endif %}"
data-frame-id="1171281209"
data-template-id="sw_reality_three_emphasis"
style="--bullet-arrow: {{ _bullet_bg | safe }};
--pill-bg: {{ _pill_bg | safe }};">
<div class="f28b__title">{{ slot_payload.title }}</div> <div class="f28b__title">{{ slot_payload.title }}</div>
<div class="f28b__cols">
<div class="f28b__col"> <div class="f28b__cards">
<div class="f28b__header">{{ slot_payload.emphasis_1_label | safe }}</div> <div class="f28b__card" style="--max-body-lines: {{ e1_lines }};">
<div class="f28b__pill">
<span class="f28b__pill-text">{{ slot_payload.emphasis_1_label | safe }}</span>
</div>
<div class="f28b__body"> <div class="f28b__body">
{% if slot_payload.emphasis_1_body %} {% if slot_payload.emphasis_1_body %}
{% for line in slot_payload.emphasis_1_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.emphasis_1_body %}<div class="text-line {% if line.indent and line.indent > 0 %}text-line--sub{% else %}text-line--heading{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="f28b__col">
<div class="f28b__header">{{ slot_payload.emphasis_2_label | safe }}</div> <div class="f28b__card" style="--max-body-lines: {{ e2_lines }};">
<div class="f28b__pill">
<span class="f28b__pill-text">{{ slot_payload.emphasis_2_label | safe }}</span>
</div>
<div class="f28b__body"> <div class="f28b__body">
{% if slot_payload.emphasis_2_body %} {% if slot_payload.emphasis_2_body %}
{% for line in slot_payload.emphasis_2_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.emphasis_2_body %}<div class="text-line {% if line.indent and line.indent > 0 %}text-line--sub{% else %}text-line--heading{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="f28b__col">
<div class="f28b__header">{{ slot_payload.emphasis_3_label | safe }}</div> <div class="f28b__card" style="--max-body-lines: {{ e3_lines }};">
<div class="f28b__pill">
<span class="f28b__pill-text">{{ slot_payload.emphasis_3_label | safe }}</span>
</div>
<div class="f28b__body"> <div class="f28b__body">
{% if slot_payload.emphasis_3_body %} {% if slot_payload.emphasis_3_body %}
{% for line in slot_payload.emphasis_3_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %} {% for line in slot_payload.emphasis_3_body %}<div class="text-line {% if line.indent and line.indent > 0 %}text-line--sub{% else %}text-line--heading{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -34,6 +34,9 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
gap: 4px; gap: 4px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif; font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all; word-break: keep-all;
/* zone aspect 기반 내부 회전 (사용자 lock 2026-05-15) */
container-type: size;
container-name: f13b-root;
} }
.f13b__title { .f13b__title {
font-size: var(--font-zone-title); font-size: var(--font-zone-title);
@@ -47,10 +50,22 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
.f13b__cols { .f13b__cols {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto;
gap: 8px; gap: 8px;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
/* zone aspect < 1.5 → 3 pillars vertical stack.
auto rows + align-content: start = 콘텐츠 height 만큼, 위쪽 정렬 (사용자 lock
2026-05-15 : 03-2 와 좌/우 visual balance 위해 콘텐츠 위로 올림). */
@container f13b-root (aspect-ratio < 1.5) {
.f13b__cols {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
grid-auto-flow: row;
align-content: start;
}
}
.f13b__col { .f13b__col {
display: flex; display: flex;
border-top: 2px solid #000; /* PROMOTED line 57 */ border-top: 2px solid #000; /* PROMOTED line 57 */
File diff suppressed because it is too large Load Diff