wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -400,7 +400,88 @@
|
||||
"Read(//tmp/**)",
|
||||
"Read(//d/tmp/**)",
|
||||
"Bash(python run_mdx03_pipeline.py --phase-z2 --run-id mvp1.5b_test5)",
|
||||
"Bash(python run_mdx03_pipeline.py --phase-z2 --run-id mvp1.5b_test7)"
|
||||
"Bash(python run_mdx03_pipeline.py --phase-z2 --run-id mvp1.5b_test7)",
|
||||
"Bash(git commit -m ' *)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\"; \"Content-Type\" = \"application/json\" }; $body = @{ state = \"open\" } | ConvertTo-Json; Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/90\" -Method Patch -Headers $headers -Body $body | Select-Object number, state, title)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $resp = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/90\" -Method Get -Headers $headers; \"#$\\($resp.number\\) state=$\\($resp.state\\) title=$\\($resp.title\\)\")",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\"; \"Content-Type\" = \"application/json\" }; $body = @{ state = \"closed\" } | ConvertTo-Json; $bytes = [System.Text.Encoding]::UTF8.GetBytes\\($body\\); Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/90\" -Method Patch -Headers $headers -Body $bytes | Select-Object number, state, closed_at, title | Format-List)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $all = @\\(\\); $page = 1; do { $url = \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues?state=closed&type=issues&page=$page&limit=50\"; $batch = Invoke-RestMethod -Uri $url -Method Get -Headers $headers; $all += $batch; $page++ } while \\($batch.Count -eq 50\\); \"Total closed issues: $\\($all.Count\\)\"; $all | Select-Object number, title | Sort-Object number | Format-Table -AutoSize -Wrap)",
|
||||
"Bash(git log *)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; foreach \\($n in 14, 64, 74, 91\\) { $r = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/$n\" -Method Get -Headers $headers; \"#$\\($r.number\\) state=$\\($r.state\\) closed_at=$\\($r.closed_at\\)\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $issue = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84\" -Method Get -Headers $headers; \"=== #84 META ===\"; \"state=$\\($issue.state\\) created=$\\($issue.created_at\\) closed=$\\($issue.closed_at\\)\"; \"title=$\\($issue.title\\)\"; \"\"; \"=== #84 BODY ===\"; $issue.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $comments = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84/comments\" -Method Get -Headers $headers; \"=== #84 COMMENT COUNT: $\\($comments.Count\\) ===\"; \"\"; $i = 0; foreach \\($c in $comments\\) { $i++; $bodyPreview = $c.body.Substring\\(0, [Math]::Min\\(200, $c.body.Length\\)\\); \"--- [$i] id=$\\($c.id\\) created=$\\($c.created_at\\) ---\"; \"$bodyPreview\"; \"\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $r = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/83\" -Method Get -Headers $headers; \"#83 state=$\\($r.state\\) closed_at=$\\($r.closed_at\\) title=$\\($r.title\\)\")",
|
||||
"Bash(git status *)",
|
||||
"Bash(python -m py_compile orchestrator.py)",
|
||||
"Bash(echo \"origin = $\\(git ls-remote origin refs/heads/main)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\"; \"Content-Type\" = \"application/json\" }; $body = @{ state = \"open\" } | ConvertTo-Json; Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/83\" -Method Patch -Headers $headers -Body $body | Select-Object number, state, closed_at, title | Format-List)",
|
||||
"Bash(git restore *)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $r83 = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/83\" -Method Get -Headers $headers; \"#83 state=$\\($r83.state\\) closed_at=[$\\($r83.closed_at\\)] labels=[$\\($r83.labels.Count\\)]\")",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84/comments?limit=100\" -Method Get -Headers $headers; \"#84 current comment count = $\\($cs.Count\\)\")",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84/comments?limit=100\" -Method Get -Headers $headers; \"Total comments now: $\\($cs.Count\\)\"; \"Recent 3:\"; $cs | Select-Object -Last 3 | ForEach-Object { $preview = $_.body.Substring\\(0, [Math]::Min\\(80, $_.body.Length\\)\\); \" id=$\\($_.id\\) [$preview...]\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $r = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84\" -Method Get -Headers $headers; \"#84 Gitea: state=$\\($r.state\\) closed_at=[$\\($r.closed_at\\)]\")",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\"; \"Content-Type\" = \"application/json\" }; $body = @{ state = \"closed\" } | ConvertTo-Json; $bytes = [System.Text.Encoding]::UTF8.GetBytes\\($body\\); Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84\" -Method Patch -Headers $headers -Body $bytes | Select-Object number, state, closed_at, title | Format-List)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $r84 = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/84\" -Method Get -Headers $headers; \"#84 state=$\\($r84.state\\) closed_at=$\\($r84.closed_at\\)\"; \"\"; \"orchestrator HEAD = $\\(git rev-parse HEAD\\)\"; \"orchestrator fix commits in chain:\"; git log --oneline -3 -- orchestrator.py)",
|
||||
"Bash(python -m py_compile src/phase_z2_pipeline.py)",
|
||||
"Bash(grep -n \"^import re\\\\|^import re$\" src/phase_z2_pipeline.py)",
|
||||
"Bash(grep -n \"^import os\\\\|^import os$\" src/phase_z2_pipeline.py)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/05.mdx mdx05_emergency_p3)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx mdx04_emergency_p3)",
|
||||
"Bash(python -c \"from src.config import settings; print\\(f'ai_fallback_enabled={settings.ai_fallback_enabled} model={settings.ai_fallback_model} has_api_key={bool\\(settings.anthropic_api_key\\)}'\\)\")",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/05.mdx mdx05_emergency_p4)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx mdx04_emergency_p4)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx mdx04_emergency_p4b)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx mdx04_p4b_full)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/03.mdx mdx03_layout_test --override-layout vertical-2)",
|
||||
"Bash(python -m src.phase_z2_pipeline samples/mdx_batch/05.mdx mdx05_layout_test --override-layout horizontal-2)",
|
||||
"Bash(cp data/user_overrides/*.json data/user_overrides_backup_20260526/)",
|
||||
"Bash(rm -f data/user_overrides/*.json)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $issue = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/94\" -Method Get -Headers $headers; \"#94 state=$\\($issue.state\\)\"; \"title=$\\($issue.title\\)\"; \"\"; \"=== body ===\"; $issue.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/94/comments\" -Method Get -Headers $headers; \"Total comments: $\\($cs.Count\\)\"; foreach \\($c in $cs | Select-Object -Last 5\\) { \"--- comment_id=$\\($c.id\\) ---\"; $c.body.Substring\\(0, [Math]::Min\\(600, $c.body.Length\\)\\); \"\" })",
|
||||
"Bash(git ls-tree *)",
|
||||
"Bash(grep -i \".html$\")",
|
||||
"Bash(grep \".html$\")",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $issue = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98\" -Method Get -Headers $headers; \"=== #$\\($issue.number\\) state=$\\($issue.state\\) ===\"; $issue.title; \"\"; \"=== BODY \\(truncated\\) ===\"; $issue.body.Substring\\(0, [Math]::Min\\(3000, $issue.body.Length\\)\\))",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $issue = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98\" -Method Get -Headers $headers; if \\($issue.body.Length -gt 3000\\) { $issue.body.Substring\\(3000, [Math]::Min\\(4000, $issue.body.Length - 3000\\)\\) })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; \"=== LATEST 2 comments ===\"; foreach \\($c in $cs | Select-Object -Last 2\\) { \"--- comment_id=$\\($c.id\\) created=$\\($c.created_at\\) ---\"; $c.body.Substring\\(0, [Math]::Min\\(3000, $c.body.Length\\)\\); \"\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; $last = $cs[-1]; if \\($last.body.Length -gt 3000\\) { $last.body.Substring\\(3000, [Math]::Min\\(4500, $last.body.Length - 3000\\)\\) })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; $last = $cs[-1]; if \\($last.body.Length -gt 7500\\) { $last.body.Substring\\(7500, $last.body.Length - 7500\\) })",
|
||||
"Bash(echo '=== task8_e2e___TRACKED_VAR__ ===')",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28849\" -Method Get -Headers $headers; \"comment_id=$\\($c.id\\) created=$\\($c.created_at\\)\"; \"\"; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; \"=== Recent comments \\(last 8\\) — id / time / first line ===\"; foreach \\($c in $cs | Select-Object -Last 8\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; foreach \\($id in 28850, 28856, 28857, 28858, 28859\\) { $c = $cs | Where-Object { $_.id -eq $id }; if \\($c\\) { \"═══════════════════════════════════════════════════════\"; \"ID $id @ $\\($c.created_at\\)\"; \"═══════════════════════════════════════════════════════\"; $c.body.Substring\\(0, [Math]::Min\\(1800, $c.body.Length\\)\\); \"\" } })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; \"=== All comments after 28850 ===\"; foreach \\($c in $cs | Where-Object { $_.id -gt 28850 }\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; foreach \\($c in $cs | Where-Object { $_.id -gt 28850 }\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28868\" -Method Get -Headers $headers; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; foreach \\($c in $cs | Where-Object { $_.id -gt 28868 }\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28869\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\)\"; \"\"; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; foreach \\($c in $cs | Where-Object { $_.id -gt 28869 }\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; foreach \\($id in 28871, 28872, 28873, 28907\\) { $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/$id\" -Method Get -Headers $headers; \"═══ [$id] @ $\\($c.created_at\\) ═══\"; $c.body.Substring\\(0, [Math]::Min\\(2000, $c.body.Length\\)\\); \"\" })",
|
||||
"Bash(python -m pytest -q tests/test_phase_z2_task15_summary_footer.py tests/test_phase_z2_task14_parent_units.py tests/test_mdx_text_atoms.py)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $cs = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/98/comments\" -Method Get -Headers $headers; \"Total: $\\($cs.Count\\)\"; \"\"; foreach \\($c in $cs | Where-Object { $_.id -gt 28907 }\\) { $first = \\($c.body -split \"`n\"\\)[0]; \"[$\\($c.id\\)] $\\($c.created_at\\) | $first\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28908\" -Method Get -Headers $headers; $c.body)",
|
||||
"Bash(python -m pytest -q tests/test_phase_z2_task18_5_semantic_sanitizer.py)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28916\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\)\"; \"\"; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; foreach \\($id in 28928, 28929\\) { $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/$id\" -Method Get -Headers $headers; \"═══ [$id] @ $\\($c.created_at\\) \\($\\($c.body.Length\\) chars\\) ═══\"; $c.body.Substring\\(0, [Math]::Min\\(3000, $c.body.Length\\)\\); \"\" })",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28955\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\) @ $\\($c.created_at\\)\"; \"\"; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/28965\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\) @ $\\($c.created_at\\)\"; \"\"; $c.body)",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/29012\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\) @ $\\($c.created_at\\)\"; \"\"; $c.body.Substring\\(0, [Math]::Min\\(2500, $c.body.Length\\)\\))",
|
||||
"PowerShell($token = [Environment]::GetEnvironmentVariable\\(\"GITEA_TOKEN\", \"User\"\\); $headers = @{ \"Authorization\" = \"token $token\" }; $c = Invoke-RestMethod -Uri \"https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/comments/29018\" -Method Get -Headers $headers; \"Length: $\\($c.body.Length\\) @ $\\($c.created_at\\)\"; \"\"; $c.body.Substring\\(0, [Math]::Min\\(2500, $c.body.Length\\)\\))",
|
||||
"Bash(python scripts/check_mdx_source_parity.py --max-items 1)",
|
||||
"Bash(sed -n '8025,8040p' src/phase_z2_pipeline.py)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task22_section_tree.py -q)",
|
||||
"Bash(python scripts/check_mdx_source_parity.py)",
|
||||
"Bash(python -c \"import json; d=json.load\\(open\\('Front/package.json'\\)\\); print\\('scripts:', d.get\\('scripts'\\)\\)\")",
|
||||
"Bash(python -c \"import json; d=json.load\\(open\\('Front_test_v515/package.json'\\)\\); print\\('name:', d.get\\('name'\\)\\); print\\('scripts:', d.get\\('scripts'\\)\\)\")",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task23_component_expansion.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task24_layout_policy.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task22_section_tree.py tests/test_phase_z2_task23_component_expansion.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task26_coverage_state.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task22_section_tree.py tests/test_phase_z2_task23_component_expansion.py tests/test_phase_z2_task24_layout_policy.py tests/test_phase_z2_task25_slot_mapping.py tests/test_phase_z2_task26_coverage_state.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task26_coverage_state.py tests/test_phase_z2_task25_slot_mapping.py tests/test_phase_z2_task23_component_expansion.py -q)",
|
||||
"Bash(npx vitest *)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task27_expand_state.py -q)",
|
||||
"Bash(python -m pytest tests/test_phase_z2_task22_section_tree.py tests/test_phase_z2_task23_component_expansion.py tests/test_phase_z2_task24_layout_policy.py tests/test_phase_z2_task25_slot_mapping.py tests/test_phase_z2_task26_coverage_state.py tests/test_phase_z2_task27_expand_state.py -q)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"d:\\ad-hoc\\kei\\design_agent\\templates\\blocks\\new",
|
||||
|
||||
@@ -16,3 +16,16 @@ data/frame_cache/*
|
||||
|
||||
# session workspace (push X — 작업 흐름 trace, 사용자 결정 2026-05-08)
|
||||
forex/
|
||||
|
||||
# 임시/생성물 정리 커밋 제외 (사용자 결정 2026-07-02)
|
||||
/tmp*.json
|
||||
/ad-hoc*.json
|
||||
/D*tmp*.json
|
||||
.orchestrator/
|
||||
chromedriver/
|
||||
*.pkl
|
||||
.claude/settings.local.json
|
||||
|
||||
# 프로젝트 전체 스냅샷 사본 (395MB/130MB — push 제외, 사용자 결정 2026-07-02)
|
||||
Front_test/
|
||||
Front_test_v515/
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
{
|
||||
"v4_source": "tests\\matching\\v4_full32_result.yaml",
|
||||
"v4_label_to_phase_z_status": {
|
||||
"use_as_is": "matched_zone",
|
||||
"light_edit": "adapt_matched_zone",
|
||||
"restructure": "extract_matched_zone",
|
||||
"reject": "fallback_candidate"
|
||||
},
|
||||
"mvp1_allowed_statuses": [
|
||||
"adapt_matched_zone",
|
||||
"matched_zone"
|
||||
],
|
||||
"mode": "composition_v0_layout_8preset",
|
||||
"mode_note": "MVP-1.5b w/ composition planner v0 — sections → candidates (separate / parent_merged) → score → greedy select → 8-preset layout vocabulary (single / horizontal-2 / vertical-2 / top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2). v0 layout = count-based; v1 axes (cardinality_fit / hierarchy_coherence / density_score) 추후.",
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_css": {
|
||||
"areas": "\"top\" \"bottom\"",
|
||||
"cols": "1fr",
|
||||
"rows": "228px 343px",
|
||||
"heights_px": [
|
||||
228,
|
||||
343
|
||||
],
|
||||
"ratios": [
|
||||
0.39,
|
||||
0.586
|
||||
],
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"dynamic_rows": true,
|
||||
"raw_zone_layout": {
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"slide_body_height": 585,
|
||||
"gap": 14,
|
||||
"available_after_gap": 571,
|
||||
"min_heights_px": [
|
||||
228,
|
||||
342
|
||||
],
|
||||
"min_scaled": true,
|
||||
"total_min_height": 570,
|
||||
"remaining_after_min": 1,
|
||||
"content_weights": [
|
||||
{
|
||||
"position": "top",
|
||||
"template_id": "three_parallel_requirements",
|
||||
"score": 4.425
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"template_id": "process_product_two_way",
|
||||
"score": 7.305
|
||||
}
|
||||
],
|
||||
"weight_shares": [
|
||||
0.377,
|
||||
0.623
|
||||
],
|
||||
"extras_px": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"heights_px": [
|
||||
228,
|
||||
343
|
||||
],
|
||||
"ratios": [
|
||||
0.39,
|
||||
0.586
|
||||
]
|
||||
}
|
||||
},
|
||||
"slide_status": {
|
||||
"rendered": true,
|
||||
"visual_check_passed": true,
|
||||
"full_mdx_coverage": true,
|
||||
"aligned_section_ids": [
|
||||
"03-1",
|
||||
"03-2"
|
||||
],
|
||||
"covered_section_ids": [
|
||||
"03-1",
|
||||
"03-2"
|
||||
],
|
||||
"filtered_section_ids": [],
|
||||
"filtered_section_reasons": [],
|
||||
"visual_fail_reasons": [],
|
||||
"adapter_needed_count": 0,
|
||||
"adapter_needed_units": [],
|
||||
"content_truncated_count": 0,
|
||||
"content_truncated_units": [],
|
||||
"overall": "PASS",
|
||||
"note": "자동 파이프라인 결과 보고. review/UI 개념 X. final.html 파일명 != PASS 의미. overall == PASS 는 visual OK + full coverage + adapter_needed=0 일 때만. adapter_needed_count > 0 = mapper 가 contract 와 안 맞아 자동 렌더 못 한 zone 존재. content_truncated_count > 0 = builder 가 truncate 한 zone 존재 (rendered 됐지만 일부 콘텐츠 손실)."
|
||||
},
|
||||
"fit_classification": {
|
||||
"visual_check_passed": true,
|
||||
"classifications": [],
|
||||
"summary": "visual check passed — no overflow to classify",
|
||||
"categories_seen": [],
|
||||
"unclassified_signals": [],
|
||||
"placement_diagnostics": [
|
||||
{
|
||||
"zone_position": "top",
|
||||
"mapper_frame_template_id": "three_parallel_requirements",
|
||||
"b4_selected_template_id": "three_parallel_requirements",
|
||||
"frame_selection_matches_mapper": true,
|
||||
"frame_selection_match_note": null,
|
||||
"region_count": 1,
|
||||
"slot_assignment_count": 1,
|
||||
"rejection_count": 0
|
||||
},
|
||||
{
|
||||
"zone_position": "bottom",
|
||||
"mapper_frame_template_id": "process_product_two_way",
|
||||
"b4_selected_template_id": "process_product_two_way",
|
||||
"frame_selection_matches_mapper": true,
|
||||
"frame_selection_match_note": null,
|
||||
"region_count": 2,
|
||||
"slot_assignment_count": 2,
|
||||
"rejection_count": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"router_decision": {
|
||||
"router_active": false,
|
||||
"proposed_actions_summary": [],
|
||||
"implementation_status_summary": {},
|
||||
"routed_count": 0,
|
||||
"routed_details": [],
|
||||
"missing_actions_pending_impl": [],
|
||||
"note": "visual check passed — no overflow to route"
|
||||
},
|
||||
"retry_trace": {
|
||||
"retry_attempted": false,
|
||||
"retry_action": null,
|
||||
"plan": null,
|
||||
"rerender_attempted": false,
|
||||
"retry_passed": false,
|
||||
"retry_failure_reason": null,
|
||||
"retried_candidate_path": null,
|
||||
"safety_margin_px": 4,
|
||||
"policy": "A3 locked rules : retry budget=1, slide-base/spacing/gap fixed, donor strict (sibling/visual ok/capacity ok/slack>0/min_height 보존), (b) revert on redistribution fail or rerender visual fail.",
|
||||
"retry_skipped_reason": "router_active=False (visual check passed — no overflow)",
|
||||
"failure_classification": {
|
||||
"failure_type": "not_attempted",
|
||||
"failure_type_description": "retry was not attempted (router_active=False or zone_ratio_retry not in proposed actions). 정상 path 의 일부 — 실패 X",
|
||||
"classification_rule": "retry_attempted=False — router_active=False or zone_ratio_retry not in proposed_actions"
|
||||
},
|
||||
"next_action_proposal": {
|
||||
"next_proposed_action": "none",
|
||||
"next_action_rationale": "retry 시도 자체가 없었음 (visual ok 등) — escalation 불필요",
|
||||
"next_action_implementation_status": "n/a",
|
||||
"mapping_source": "A4 NEXT_ACTION_BY_FAILURE (사용자 잠금 2026-04-29)"
|
||||
}
|
||||
},
|
||||
"composition_planner_debug": {
|
||||
"planner_version": "v0.2",
|
||||
"selection_rule": "score desc, then source_section_ids count desc (coverage tiebreak). filter = phase_z_status ∉ allowed_statuses OR auto_selectable=False. auto_selectable=False 사유 : C1 (capacity mismatch — silent truncate / FitError 차단), W1 (rep not auto-renderable), W2 (all children reject), W3 (majority children non-auto-renderable).",
|
||||
"candidates_total": 2,
|
||||
"candidates_viable_auto": 2,
|
||||
"candidates_summary": [
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "three_parallel_requirements",
|
||||
"label": "use_as_is",
|
||||
"phase_z_status": "matched_zone",
|
||||
"score": 0.9268,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 3,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 3,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "process_product_two_way",
|
||||
"label": "use_as_is",
|
||||
"phase_z_status": "matched_zone",
|
||||
"score": 0.9198,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"merge_candidates": [],
|
||||
"capacity_mismatches": [],
|
||||
"selected_units_count": 2,
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_preset_rationale": "v0 count-based: 2 units → horizontal-2"
|
||||
},
|
||||
"zones": [
|
||||
{
|
||||
"position": "top",
|
||||
"source_section_ids": [
|
||||
"03-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"title": "1. DX 시행을 위한 필수 요건",
|
||||
"v4_rank1_frame_id": "1171281190",
|
||||
"v4_rank1_frame_number": 13,
|
||||
"v4_template_id": "three_parallel_requirements",
|
||||
"v4_label": "use_as_is",
|
||||
"v4_confidence": 0.9268,
|
||||
"phase_z_status": "matched_zone",
|
||||
"composition_score": 0.9268,
|
||||
"composition_rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 3,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 3,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.9268,
|
||||
"confidence": 0.9268,
|
||||
"label": "use_as_is",
|
||||
"label_weight": 1.0,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"composition_notes": [],
|
||||
"mapper_type": "contract",
|
||||
"contract_id": "three_parallel_requirements",
|
||||
"contract_frame_id": 1171281190,
|
||||
"builder": "items_with_role",
|
||||
"min_height_px": 230,
|
||||
"slot_payload_keys": [
|
||||
"pillars",
|
||||
"title"
|
||||
],
|
||||
"content_truncated_count": null,
|
||||
"assets_dir": null,
|
||||
"content_weight": {
|
||||
"score": 4.425,
|
||||
"text_length": 660,
|
||||
"top_bullets": 3,
|
||||
"nested_bullets": 16,
|
||||
"has_table": false,
|
||||
"subsection_count": 0
|
||||
},
|
||||
"placement_trace": {
|
||||
"section_id": "03-1",
|
||||
"selected_frame_id": "1171281190",
|
||||
"selected_template_id": "three_parallel_requirements",
|
||||
"internal_regions": [
|
||||
{
|
||||
"region_id": "03-1.region-1",
|
||||
"role": "primary",
|
||||
"content_type": "text_block",
|
||||
"ratio_estimate": 1.0,
|
||||
"content_unit_ids": [
|
||||
"03-1.text-1"
|
||||
],
|
||||
"frame_match_strategy": {
|
||||
"kind": "frame_match",
|
||||
"frame_id": null,
|
||||
"display_strategy": "inline_full"
|
||||
},
|
||||
"source_shape_index": null
|
||||
}
|
||||
],
|
||||
"slot_assignments": [
|
||||
{
|
||||
"region_id": "03-1.region-1",
|
||||
"content_unit_id": "03-1.text-1",
|
||||
"frame_slot_id": "pillar_1",
|
||||
"partial_target_path": ".f13b__cols > .f13b__col:nth-child(1)",
|
||||
"display_strategy": "inline_full"
|
||||
}
|
||||
],
|
||||
"overflow_buffer": [],
|
||||
"rejection": [],
|
||||
"mapper_frame_template_id": "three_parallel_requirements",
|
||||
"frame_selection_matches_mapper": true,
|
||||
"frame_selection_match_note": null
|
||||
},
|
||||
"height_px": 228,
|
||||
"ratio": 0.39
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"source_section_ids": [
|
||||
"03-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"title": "2. Process의 혁신과 Product의 변화",
|
||||
"v4_rank1_frame_id": "1171281210",
|
||||
"v4_rank1_frame_number": 29,
|
||||
"v4_template_id": "process_product_two_way",
|
||||
"v4_label": "use_as_is",
|
||||
"v4_confidence": 0.9198,
|
||||
"phase_z_status": "matched_zone",
|
||||
"composition_score": 0.9198,
|
||||
"composition_rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.9198,
|
||||
"confidence": 0.9198,
|
||||
"label": "use_as_is",
|
||||
"label_weight": 1.0,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"composition_notes": [],
|
||||
"mapper_type": "contract",
|
||||
"contract_id": "process_product_two_way",
|
||||
"contract_frame_id": 1171281210,
|
||||
"builder": "process_product_pair",
|
||||
"min_height_px": 345,
|
||||
"slot_payload_keys": [
|
||||
"banner_left",
|
||||
"banner_right",
|
||||
"process",
|
||||
"product",
|
||||
"title"
|
||||
],
|
||||
"content_truncated_count": null,
|
||||
"assets_dir": "assets\\process_product_two_way",
|
||||
"content_weight": {
|
||||
"score": 7.305,
|
||||
"text_length": 924,
|
||||
"top_bullets": 6,
|
||||
"nested_bullets": 7,
|
||||
"has_table": true,
|
||||
"subsection_count": 2
|
||||
},
|
||||
"placement_trace": {
|
||||
"section_id": "03-2",
|
||||
"selected_frame_id": "1171281210",
|
||||
"selected_template_id": "process_product_two_way",
|
||||
"internal_regions": [
|
||||
{
|
||||
"region_id": "03-2.region-1",
|
||||
"role": "primary",
|
||||
"content_type": "text_block",
|
||||
"ratio_estimate": 0.8421,
|
||||
"content_unit_ids": [
|
||||
"03-2.text-1"
|
||||
],
|
||||
"frame_match_strategy": {
|
||||
"kind": "frame_match",
|
||||
"frame_id": null,
|
||||
"display_strategy": "inline_full"
|
||||
},
|
||||
"source_shape_index": null
|
||||
},
|
||||
{
|
||||
"region_id": "03-2.region-2",
|
||||
"role": "supporting",
|
||||
"content_type": "transform_table",
|
||||
"ratio_estimate": 0.1579,
|
||||
"content_unit_ids": [
|
||||
"03-2.transform-1"
|
||||
],
|
||||
"frame_match_strategy": {
|
||||
"kind": "frame_match",
|
||||
"frame_id": null,
|
||||
"display_strategy": "inline_full"
|
||||
},
|
||||
"source_shape_index": null
|
||||
}
|
||||
],
|
||||
"slot_assignments": [
|
||||
{
|
||||
"region_id": "03-2.region-1",
|
||||
"content_unit_id": "03-2.text-1",
|
||||
"frame_slot_id": "product_column",
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--right",
|
||||
"display_strategy": "inline_full"
|
||||
},
|
||||
{
|
||||
"region_id": "03-2.region-2",
|
||||
"content_unit_id": "03-2.transform-1",
|
||||
"frame_slot_id": "process_column",
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--left",
|
||||
"display_strategy": "inline_full"
|
||||
}
|
||||
],
|
||||
"overflow_buffer": [],
|
||||
"rejection": [],
|
||||
"mapper_frame_template_id": "process_product_two_way",
|
||||
"frame_selection_matches_mapper": true,
|
||||
"frame_selection_match_note": null
|
||||
},
|
||||
"height_px": 343,
|
||||
"ratio": 0.586
|
||||
}
|
||||
],
|
||||
"visual_runtime_check": {
|
||||
"frame_slot_metrics": [
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--left",
|
||||
"clientHeight": 125,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "process_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 125,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
},
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--right",
|
||||
"clientHeight": 125,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "product_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 125,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
},
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--left",
|
||||
"clientHeight": 43,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "process_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 43,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
},
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--right",
|
||||
"clientHeight": 43,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "product_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 43,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
},
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--left",
|
||||
"clientHeight": 63,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "process_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 63,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
},
|
||||
{
|
||||
"class_name": "f29b__cell f29b__cell--right",
|
||||
"clientHeight": 63,
|
||||
"clientWidth": 577,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"frame_slot_id": "product_column",
|
||||
"overflowed": false,
|
||||
"scrollHeight": 63,
|
||||
"scrollWidth": 577,
|
||||
"zone_position": "bottom",
|
||||
"zone_template_id": "process_product_two_way"
|
||||
}
|
||||
],
|
||||
"slide": {
|
||||
"clientHeight": 720,
|
||||
"clientWidth": 1280,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"overflowed": false,
|
||||
"scrollHeight": 720,
|
||||
"scrollWidth": 1280,
|
||||
"size_correct": true
|
||||
},
|
||||
"slide_body": {
|
||||
"clientHeight": 585,
|
||||
"clientWidth": 1180,
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"overflowed": false,
|
||||
"scrollHeight": 585,
|
||||
"scrollWidth": 1180
|
||||
},
|
||||
"zones": [
|
||||
{
|
||||
"clientHeight": 228,
|
||||
"clientWidth": 1180,
|
||||
"clipped_inner": [],
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"overflowed": false,
|
||||
"position": "top",
|
||||
"scrollHeight": 228,
|
||||
"scrollWidth": 1180,
|
||||
"template_id": "three_parallel_requirements"
|
||||
},
|
||||
{
|
||||
"clientHeight": 343,
|
||||
"clientWidth": 1180,
|
||||
"clipped_inner": [],
|
||||
"excess_x": 0,
|
||||
"excess_y": 0,
|
||||
"overflowed": false,
|
||||
"position": "bottom",
|
||||
"scrollHeight": 343,
|
||||
"scrollWidth": 1180,
|
||||
"template_id": "process_product_two_way"
|
||||
}
|
||||
],
|
||||
"screenshot": "data\\runs\\mdx03_f29_fix_check\\phase_z2\\preview.png",
|
||||
"passed": true,
|
||||
"fail_reasons": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,927 @@
|
||||
<!-- Phase Z-2 MVP-1.5b — single slide + Type B layout (top/bottom zones).
|
||||
원래 Phase Z 설계 복귀: MDX 1 = slide 1, layout preset = zone 분할, frame-derived block ⊂ zone. -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1280">
|
||||
<title>DX 실행 체계 구축 방안</title>
|
||||
<style>
|
||||
/* ── existing tokens (inlined) ── */
|
||||
/* === typography.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Typography Tokens v1
|
||||
══════════════════════════════════════
|
||||
슬라이드 전체의 공통 글자 위계.
|
||||
모든 블록은 이 토큰을 참조한다.
|
||||
직접값(font-size: 11px) 금지 → var(--font-body) 사용.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── 1층: Global Hierarchy ── */
|
||||
|
||||
/* 대목차: 슬라이드 상단 제목 */
|
||||
--font-slide-title: 22px;
|
||||
--weight-slide-title: 700;
|
||||
--lh-slide-title: 1.4;
|
||||
|
||||
/* 중목차: zone 제목 */
|
||||
--font-zone-title: 16px;
|
||||
--weight-zone-title: 700;
|
||||
--lh-zone-title: 1.4;
|
||||
|
||||
/* 소목차: 블록 내 소제목, 카드 제목 */
|
||||
--font-sub-title: 12px;
|
||||
--weight-sub-title: 700;
|
||||
--lh-sub-title: 1.45;
|
||||
|
||||
/* 본문: 블릿, 설명 텍스트 */
|
||||
--font-body: 10px;
|
||||
--weight-body: 500;
|
||||
--lh-body: 1.55;
|
||||
|
||||
/* 캡션: 각주, 출처, 보조 텍스트 */
|
||||
--font-caption: 10px;
|
||||
--weight-caption: 400;
|
||||
--lh-caption: 1.4;
|
||||
|
||||
/* footer: 핵심 인사이트 pill */
|
||||
--font-footer: 20px;
|
||||
--weight-footer: 700;
|
||||
--lh-footer: 1.2;
|
||||
|
||||
/* ── 2층: Component Semantic Tokens ── */
|
||||
/* 가까운 위계에서 기본값, 필요시 override */
|
||||
|
||||
/* 본문 강조 (인라인 heading) */
|
||||
--font-body-strong: var(--font-body);
|
||||
--weight-body-strong: 600;
|
||||
|
||||
/* 자세히보기 링크 */
|
||||
--font-detail-link: var(--font-caption);
|
||||
--weight-detail-link: 500;
|
||||
|
||||
/* pill/badge 라벨 */
|
||||
--font-pill-label: var(--font-sub-title);
|
||||
--weight-pill-label: 700;
|
||||
|
||||
/* 표 헤더 */
|
||||
--font-table-header: var(--font-sub-title);
|
||||
--weight-table-header: 700;
|
||||
|
||||
/* 표 셀 */
|
||||
--font-table-cell: var(--font-body);
|
||||
--weight-table-cell: 400;
|
||||
|
||||
/* 비교 뱃지 */
|
||||
--font-compare-badge: var(--font-sub-title);
|
||||
--weight-compare-badge: 700;
|
||||
|
||||
/* 강조 인용 */
|
||||
--font-callout: var(--font-sub-title);
|
||||
--weight-callout: 700;
|
||||
|
||||
/* 상단 라벨 (overline) */
|
||||
--font-overline: var(--font-caption);
|
||||
--weight-overline: 600;
|
||||
--ls-overline: 0.05em;
|
||||
}
|
||||
|
||||
|
||||
/* === spacing.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Spacing Tokens v1
|
||||
══════════════════════════════════════
|
||||
여백/간격/들여쓰기 공통 기준.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── 기본 스케일 ── */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 24px;
|
||||
|
||||
/* ── slide-base 레이아웃 ── */
|
||||
--slide-padding-x: 40px; /* 본문 좌우 여백 */
|
||||
--slide-title-left: 52px; /* 대목차 시작 */
|
||||
--slide-title-top: 22px; /* 대목차 위치 */
|
||||
--slide-divider-top: 58px; /* 구분선 위치 (slide_base.html 와 일치 2026-05-07) */
|
||||
--slide-body-top: 76px; /* 본문 시작 (사용자 직설: divider-body 16px) */
|
||||
--slide-body-height: 585px; /* 본문 높이 (사용자 직설: body-footer 10px) */
|
||||
--slide-footer-bottom: 8px; /* footer 하단 여백 (slide_base.html 와 일치) */
|
||||
--slide-footer-height: 41px; /* footer 높이 */
|
||||
|
||||
/* ── zone ── */
|
||||
--zone-gap: 8px; /* zone 간 간격 */
|
||||
--zone-padding-left: 12px; /* zone 안쪽 좌측 여백 */
|
||||
--zone-padding-right: 8px; /* zone 안쪽 우측 여백 */
|
||||
--zone-title-mb: 8px; /* zone 제목 아래 여백 */
|
||||
|
||||
/* ── 블록 공통 ── */
|
||||
--card-padding: 16px; /* 카드 내부 여백 */
|
||||
--card-padding-sm: 8px; /* 작은 카드 내부 여백 */
|
||||
--card-gap: 12px; /* 카드 간 간격 */
|
||||
--card-radius: 6px; /* 카드 모서리 */
|
||||
|
||||
/* ── 블릿/텍스트 ── */
|
||||
--bullet-indent: 14px; /* 불릿 hanging indent */
|
||||
--bullet-gap: 2px; /* 불릿 항목 간 간격 */
|
||||
--heading-gap: 4px; /* 소제목 아래 간격 */
|
||||
--section-gap: 10px; /* 섹션 간 간격 */
|
||||
|
||||
/* ── flex/grid 기본 ── */
|
||||
--flex-gap: 12px; /* 기본 flex gap */
|
||||
--flex-gap-sm: 6px; /* 좁은 flex gap */
|
||||
--grid-gap: 16px; /* 기본 grid gap */
|
||||
}
|
||||
|
||||
|
||||
/* === colors.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Color Tokens v1
|
||||
══════════════════════════════════════
|
||||
2층 구조: 공통 테마색 + 블록 의미색.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ══ 1층: 공통 테마색 ══ */
|
||||
/* 모든 블록이 공유하는 기본 팔레트 */
|
||||
|
||||
/* 텍스트 */
|
||||
--color-title: #1e293b; /* 제목 (진한 남색) */
|
||||
--color-zone-title: #1a365d; /* zone 중목차 제목 */
|
||||
--color-body: #475569; /* 본문 */
|
||||
--color-body-strong: #1e293b; /* 본문 강조 */
|
||||
--color-caption: #94a3b8; /* 캡션, 보조 */
|
||||
--color-muted: #64748b; /* 약한 텍스트 */
|
||||
|
||||
/* 배경 */
|
||||
--color-bg: #ffffff; /* 슬라이드 배경 */
|
||||
--color-bg-subtle: #f8fafc; /* 카드/영역 배경 */
|
||||
--color-bg-page: #e8ecf0; /* 페이지 배경 (슬라이드 바깥) */
|
||||
|
||||
/* 보더 */
|
||||
--color-border: #e2e8f0; /* 기본 보더 */
|
||||
--color-border-strong: #cbd5e1; /* 강한 보더 */
|
||||
|
||||
/* 강조 */
|
||||
--color-accent: #2563eb; /* 링크, 강조 (파랑) */
|
||||
--color-accent-hover: #eff6ff; /* 링크 hover 배경 */
|
||||
--color-danger: #dc2626; /* 경고, 에러 */
|
||||
|
||||
/* slide-base 전용 */
|
||||
--color-slide-title-from: #296b55; /* 대목차 gradient 시작 */
|
||||
--color-slide-title-to: #123328; /* 대목차 gradient 끝 */
|
||||
--color-footer-bg-from: #3b3523; /* footer gradient 시작 */
|
||||
--color-footer-bg-mid: #263a2a; /* footer gradient 중간 */
|
||||
--color-footer-bg-to: #113f31; /* footer gradient 끝 */
|
||||
--color-footer-text: #ffffff; /* footer 텍스트 */
|
||||
--color-footer-accent: #ffee33; /* footer 강조 (em) */
|
||||
|
||||
/* ══ 2층: 블록 의미색 ══ */
|
||||
/* 특정 블록 유형에서 의미를 가지는 색 */
|
||||
|
||||
/* 3열 비교 (prerequisites-3col) */
|
||||
--color-col-1-from: #0D78D0; /* 1열 gradient 시작 */
|
||||
--color-col-1-to: #023056; /* 1열 gradient 끝 */
|
||||
--color-col-2-from: #FF9A23; /* 2열 gradient 시작 */
|
||||
--color-col-2-to: #CC5200; /* 2열 gradient 끝 */
|
||||
--color-col-3-from: #39BE49; /* 3열 gradient 시작 */
|
||||
--color-col-3-to: #23742C; /* 3열 gradient 끝 */
|
||||
|
||||
/* 비교 블록 (compare) */
|
||||
--color-compare-left: #5c3714; /* 좌측 (AS-IS 계열) */
|
||||
--color-compare-right: #285b4a; /* 우측 (TO-BE 계열) */
|
||||
--color-compare-badge: #ae3607; /* VS 뱃지 */
|
||||
|
||||
/* 다크 카드 (direct render) */
|
||||
--color-dark-card-1: #1a365d; /* 카드 1 배경 */
|
||||
--color-dark-card-2: #1e3a2f; /* 카드 2 배경 */
|
||||
--color-dark-card-3: #3b1f2b; /* 카드 3 배경 */
|
||||
--color-dark-card-title: #fbbf24; /* 다크카드 제목 (금색) */
|
||||
--color-dark-card-body: #e2e8f0; /* 다크카드 본문 */
|
||||
|
||||
/* 표 */
|
||||
--color-table-header-bg: #64748b; /* 표 헤더 배경 */
|
||||
--color-table-header-text: #ffffff; /* 표 헤더 텍스트 */
|
||||
|
||||
/* 블록 제목 gradient (Figma 원본) */
|
||||
--color-block-title-from: #CC5200; /* 주황 gradient 시작 */
|
||||
--color-block-title-to: #883700; /* 주황 gradient 끝 */
|
||||
|
||||
/* pill */
|
||||
--color-pill-bg: #1e293b; /* pill 배경 */
|
||||
--color-pill-text: #ffffff; /* pill 텍스트 */
|
||||
}
|
||||
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
background: #e8ecf0;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
min-height: 100vh;
|
||||
word-break: keep-all;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* ── 16:9 슬라이드 (single, 1280×720) ── */
|
||||
.slide {
|
||||
width: 1280px; height: 720px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.slide-bg {
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
.slide-title {
|
||||
position: absolute;
|
||||
left: 52px; top: 22px;
|
||||
width: calc(100% - 104px);
|
||||
font-weight: 700;
|
||||
font-size: var(--font-slide-title);
|
||||
line-height: var(--lh-slide-title);
|
||||
background-image: linear-gradient(180deg, #296b55 0%, #123328 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 2px #322c1e;
|
||||
z-index: 2;
|
||||
}
|
||||
.slide-divider {
|
||||
position: absolute;
|
||||
left: 50px; top: 58px;
|
||||
width: calc(100% - 100px);
|
||||
height: 2px;
|
||||
background: #cbd5e1;
|
||||
z-index: 2;
|
||||
}
|
||||
.slide-body {
|
||||
position: absolute;
|
||||
left: 50px; top: 76px;
|
||||
width: calc(100% - 100px); /* 1180 */
|
||||
height: 585px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Layout preset (composition planner v0 — 8 vocabulary) ──
|
||||
단일 .layout-{preset} 클래스에 grid CSS 주입.
|
||||
layout_css = pipeline build_layout_css() 결과 :
|
||||
- horizontal-2 : rows = dynamic px (content_weight + frame_min_height)
|
||||
- 그 외 : rows / cols = fr default (LAYOUT_PRESETS)
|
||||
positions (single / top / bottom / left / right / top-left / ... / grid-2x2 9개)
|
||||
은 .zone 의 inline grid-area 로 지정 — preset CSS 는 areas 만 정의. */
|
||||
.layout-horizontal-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 228px 343px;
|
||||
grid-template-areas: "top" "bottom";
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Zone — block 가용 공간 최대화. 프레임 자체 styling 이 boundary 역할 ── */
|
||||
.zone {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Frame-family text layout contract (shared, reusable) ──
|
||||
feedback-1 (mvp1.5b_test7): visible improvement 강화.
|
||||
Stronger hanging indent + breathing line spacing + visible hierarchy. */
|
||||
.text-line {
|
||||
font-size: var(--font-body); /* 11px */
|
||||
font-weight: var(--weight-body);
|
||||
line-height: 1.6; /* breathing room */
|
||||
margin: 0;
|
||||
}
|
||||
.text-line + .text-line {
|
||||
margin-top: 4px; /* visible gap between lines (was 2px) */
|
||||
}
|
||||
|
||||
/* bullet line — hanging indent via padding + absolute marker (안정적) */
|
||||
.text-line--bullet {
|
||||
padding-left: 14px;
|
||||
position: relative;
|
||||
}
|
||||
.text-line--bullet::before {
|
||||
content: "•"; /* visible bullet 마커 */
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 0;
|
||||
font-weight: 700;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
/* indent levels — visible nested hierarchy */
|
||||
.text-line--indent-1 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.text-line--indent-1::before {
|
||||
content: "◦"; /* sub-bullet 다른 마커 */
|
||||
left: 18px;
|
||||
}
|
||||
.text-line--indent-2 {
|
||||
padding-left: 46px;
|
||||
}
|
||||
.text-line--indent-2::before {
|
||||
content: "▪";
|
||||
left: 34px;
|
||||
}
|
||||
/* body line — 마커 없음 */
|
||||
.text-line--body {
|
||||
padding-left: 0;
|
||||
}
|
||||
.text-line strong { color: #000; font-weight: 700; }
|
||||
|
||||
/* ── Transform-block family (frame 29 AS-IS/TO-BE dedicated comparison component) ──
|
||||
feedback-3: bullet rows 가 아닌 *전용 비교 component*. Header (AS-IS / TO-BE) + paired rows.
|
||||
color-coded cells, left-border accent → 단번에 *비교 구조* 인지. */
|
||||
.transform-block {
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.transform-block__header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 22px 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px dashed rgba(0,0,0,0.12);
|
||||
font-size: 9px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.transform-block__label--from { color: #6b5444; }
|
||||
.transform-block__label--to { color: #2563eb; text-align: left; }
|
||||
.transform-rows {
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.transform-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 22px 1fr;
|
||||
gap: 6px;
|
||||
align-items: stretch;
|
||||
font-size: var(--font-body);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.transform-row__from {
|
||||
padding: 3px 8px;
|
||||
background: linear-gradient(90deg, rgba(140,120,90,0.16), rgba(140,120,90,0.08));
|
||||
border-left: 2px solid #b8a98a;
|
||||
border-radius: 2px;
|
||||
color: #4b3f30;
|
||||
font-weight: 500;
|
||||
}
|
||||
.transform-row__arrow {
|
||||
text-align: center;
|
||||
color: #2563eb;
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
align-self: center;
|
||||
}
|
||||
.transform-row__to {
|
||||
padding: 3px 8px;
|
||||
background: linear-gradient(90deg, rgba(37,99,235,0.10), rgba(37,99,235,0.18));
|
||||
border-left: 2px solid #2563eb;
|
||||
border-radius: 2px;
|
||||
color: #1e40af;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Footer pill — feedback-3: body 와 weight balance, 살짝 가벼워짐 ── */
|
||||
.slide-footer {
|
||||
position: absolute;
|
||||
left: 50px; bottom: 8px;
|
||||
width: calc(100% - 100px); /* front 기준 (body 와 정렬) */
|
||||
height: 41px; /* front 기준 */
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(90deg, #3b3523 5%, #263a2a 50%, #113f31 95%);
|
||||
opacity: 0.92; /* slightly lighter */
|
||||
}
|
||||
.slide-footer-text {
|
||||
position: relative; z-index: 1;
|
||||
font-size: 15px; /* was 20px (--font-footer) */
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 3px rgba(0,0,0,.4);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.slide-footer-text em { color: #fe3; font-style: normal; }
|
||||
|
||||
.phase-z2-marker {
|
||||
position: absolute;
|
||||
top: 4px; right: 8px;
|
||||
font-size: 9px; color: #94a3b8;
|
||||
font-family: monospace;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide" data-page="1">
|
||||
<div class="slide-bg"></div>
|
||||
<div class="phase-z2-marker">phase_z2 / mvp-1.5b / horizontal-2 / single slide</div>
|
||||
<div class="slide-title">DX 실행 체계 구축 방안</div>
|
||||
<div class="slide-divider"></div>
|
||||
<div class="slide-body">
|
||||
<div class="layout-horizontal-2">
|
||||
|
||||
<div class="zone" data-zone-position="top" data-template-id="three_parallel_requirements" style="grid-area: top;">
|
||||
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
|
||||
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt.
|
||||
Legacy templates/blocks/structures/prerequisites-3col.html 의 token-fixed font 접근 학습. -->
|
||||
|
||||
|
||||
<style>
|
||||
.f13b {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.f13b__title {
|
||||
font-size: var(--font-zone-title);
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-zone-title);
|
||||
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.f13b__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.f13b__col {
|
||||
display: flex;
|
||||
border-top: 2px solid #000; /* PROMOTED line 57 */
|
||||
border-bottom: 2px solid #000;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.f13b__bar {
|
||||
flex: 0 0 50px;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 6px 2px;
|
||||
gap: 3px;
|
||||
}
|
||||
.f13b__col--tech .f13b__bar { background: linear-gradient(180deg, #0D78D0 0%, #023056 100%); }
|
||||
.f13b__col--people .f13b__bar { background: linear-gradient(180deg, #FF9A23 0%, #CC5200 100%); }
|
||||
.f13b__col--nature .f13b__bar { background: linear-gradient(180deg, #39BE49 21%, #23742C 100%); }
|
||||
/* PROMOTED Figma frame 13 line 92-93 — main + parenthetical stacked vertically */
|
||||
.f13b__bar-main {
|
||||
font-size: var(--font-sub-title); /* 12px — main word "기술/사람/자연" */
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
}
|
||||
.f13b__bar-paren {
|
||||
font-size: var(--font-caption); /* 10px — "(디지털/역량/여건)" */
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.f13b__body {
|
||||
flex: 1;
|
||||
display: flex; flex-direction: column;
|
||||
padding: 8px 12px; /* breathing (was 6px 10px) */
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f13b__section {
|
||||
flex: 1;
|
||||
padding: 6px 0; /* breathing (was 4px) */
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.f13b__section + .f13b__section {
|
||||
border-top: 2px dashed #000; /* PROMOTED line 103 */
|
||||
margin-top: 6px; /* visible section gap (was 4px) */
|
||||
padding-top: 8px; /* breathing (was 6px) */
|
||||
}
|
||||
/* heading — 부소목차 (사용자 5-layer 직설 2026-05-07): Digital 기술과... 같은 깊이 1 굵은 bullet */
|
||||
.f13b__heading {
|
||||
font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-sub-title);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
margin-bottom: 6px; /* visible heading-body gap (was 4px) */
|
||||
}
|
||||
.f13b__col--tech .f13b__heading { background-image: linear-gradient(180deg, #0D78D0 0%, #134D7F 100%); }
|
||||
.f13b__col--people .f13b__heading { background-image: linear-gradient(180deg, #FE900C 0%, #D18B37 100%); }
|
||||
.f13b__col--nature .f13b__heading { background-image: linear-gradient(180deg, #3CA649 21%, #23742C 100%); }
|
||||
|
||||
/* desc — frame-family text layout contract (.text-line 공통 클래스 사용)
|
||||
feedback (mvp1.5b_test5): browser-native ul/li 폐기. */
|
||||
.f13b__desc {
|
||||
color: #3E3523; /* PROMOTED line 118 */
|
||||
overflow: hidden;
|
||||
}
|
||||
/* desc 안 .text-line 색 override */
|
||||
.f13b__desc .text-line { color: #3E3523; }
|
||||
</style>
|
||||
|
||||
<div class="f13b" data-frame-id="1171281190" data-template-id="three_parallel_requirements">
|
||||
<div class="f13b__title">1. DX 시행을 위한 필수 요건</div>
|
||||
<div class="f13b__cols">
|
||||
|
||||
<div class="f13b__col f13b__col--tech">
|
||||
<div class="f13b__bar">
|
||||
<span class="f13b__bar-main">기술</span>
|
||||
|
||||
<span class="f13b__bar-paren">(디지털)</span>
|
||||
|
||||
</div>
|
||||
<div class="f13b__body">
|
||||
|
||||
<div class="f13b__section">
|
||||
<div class="f13b__heading">Digital 기술(S/W, H/W)과 업무 Process의 통합</div>
|
||||
|
||||
<div class="f13b__desc">
|
||||
<div class="text-line text-line--bullet">기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행</div><div class="text-line text-line--bullet">프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f13b__section">
|
||||
<div class="f13b__heading">분야별 전문 지식(설계, 시공, 유지관리 등) 보유</div>
|
||||
|
||||
<div class="f13b__desc">
|
||||
<div class="text-line text-line--bullet">건설 전 단계에 대한 근본적인 이해와 지식 및 경험</div><div class="text-line text-line--bullet">최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f13b__col f13b__col--people">
|
||||
<div class="f13b__bar">
|
||||
<span class="f13b__bar-main">사람</span>
|
||||
|
||||
<span class="f13b__bar-paren">(역량)</span>
|
||||
|
||||
</div>
|
||||
<div class="f13b__body">
|
||||
|
||||
<div class="f13b__section">
|
||||
<div class="f13b__heading">혁신적 사고방식과 창의적 문제 해결 능력</div>
|
||||
|
||||
<div class="f13b__desc">
|
||||
<div class="text-line text-line--bullet">기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피</div><div class="text-line text-line--bullet">디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f13b__section">
|
||||
<div class="f13b__heading">사용자 중심 사고와 DX 수행 경험</div>
|
||||
|
||||
<div class="f13b__desc">
|
||||
<div class="text-line text-line--bullet">사용자의 요구와 기대를 충족시키는 설계 및 구현</div><div class="text-line text-line--bullet">시행착오를 포함한 수행 경험과 사용자 경험(UX)을 반영한 해결 방안 제시</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f13b__col f13b__col--nature">
|
||||
<div class="f13b__bar">
|
||||
<span class="f13b__bar-main">자연</span>
|
||||
|
||||
<span class="f13b__bar-paren">(여건)</span>
|
||||
|
||||
</div>
|
||||
<div class="f13b__body">
|
||||
|
||||
<div class="f13b__section">
|
||||
<div class="f13b__heading">지속적인 투자 및 실행 의지</div>
|
||||
|
||||
<div class="f13b__desc">
|
||||
<div class="text-line text-line--bullet">기술 도입 초기 단계에 필요한 인력·기간·비용 등의 대규모 투자</div><div class="text-line text-line--bullet">기술 고도화를 위한 지속적인 개선 및 투자 체계 구축</div><div class="text-line text-line--bullet">변화와 혁신을 통해 부가가치를 창출하려는 실행 의지와 추진력</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone" data-zone-position="bottom" data-template-id="process_product_two_way" style="grid-area: bottom;">
|
||||
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
|
||||
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
|
||||
|
||||
|
||||
<style>
|
||||
.f29b {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
word-break: keep-all;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f29b__title {
|
||||
font-size: var(--font-zone-title); /* 13px — frame 13 의 f13b__title 과 동일 위계 */
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-zone-title);
|
||||
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
flex-shrink: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* banners — top */
|
||||
.f29b__banners {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
}
|
||||
.f29b__banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
/* PROMOTED background gradients — figma origin lines 84-98 (header-bg--left/right).
|
||||
원본 SVG 3 개 → CSS gradient 로 재현 (R8/R9 준수). border-radius 9999px = 자동 pill. */
|
||||
.f29b__banner--left {
|
||||
background: linear-gradient(to right,
|
||||
rgba(136, 131, 118, 0.343) 0%,
|
||||
#39321E 73.1%,
|
||||
#39321E 100%);
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
}
|
||||
.f29b__banner--right {
|
||||
background: linear-gradient(to right,
|
||||
#032118 0%,
|
||||
rgba(41, 107, 85, 0.1) 100%);
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
.f29b__banner-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 2px #322C1E;
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* PROMOTED gradient — lines 101-104 / 110-113 */
|
||||
.f29b__banner--left .f29b__banner-text {
|
||||
background-image:
|
||||
linear-gradient(90deg, rgb(62,53,35) 0%, rgb(62,53,35) 100%),
|
||||
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||
}
|
||||
.f29b__banner--right .f29b__banner-text {
|
||||
background-image:
|
||||
linear-gradient(90deg, rgb(34,95,74) 0%, rgb(34,95,74) 100%),
|
||||
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||
}
|
||||
|
||||
/* body — 6 cell grid (3 rows × 2 cols) — 좌우 row divider Y 정렬 */
|
||||
/* PROMOTED background — figma origin lines 53-68 (body-bg-left/right).
|
||||
좌(0~50%): #39321E α0.3 (top) → white α0.3 (bottom)
|
||||
우(50~100%): #296B55 α0.3 (top) → white α0.3 (bottom)
|
||||
원본 SVG → CSS multi-bg 로 재현. */
|
||||
.f29b__body {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
padding-top: 4px;
|
||||
background:
|
||||
linear-gradient(to bottom, rgba(57,50,30,0.3) 0%, rgba(255,255,255,0.3) 100%) 0% 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(좌, 우) */
|
||||
.f29b__grid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: auto;
|
||||
gap: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
height: 100%;
|
||||
align-content: start;
|
||||
}
|
||||
.f29b__cell {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
/* row 2/3 (3 번째 cell 이후) 위에 dashed divider — 좌우 같은 Y */
|
||||
.f29b__cell:nth-child(n+3) {
|
||||
border-top: 1px dashed rgba(0,0,0,0.15);
|
||||
padding-top: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
/* section-title — 부소목차 (사용자 5-layer 직설 2026-05-07): Analogue 기반... 같은 깊이 0 굵은 bullet */
|
||||
.f29b__section-title {
|
||||
font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 6px;
|
||||
border-left: 3px solid currentColor;
|
||||
}
|
||||
.f29b__cell--left .f29b__section-title { color: #5C3714; } /* PROMOTED line 125 */
|
||||
.f29b__cell--right .f29b__section-title { color: #084C56; } /* PROMOTED line 126 */
|
||||
.f29b__section-title__sub {
|
||||
font-size: var(--font-caption); /* 10px */
|
||||
margin-left: 3px;
|
||||
}
|
||||
/* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override. */
|
||||
.f29b__cell .text-line--bullet { color: #000; font-weight: 700; }
|
||||
|
||||
/* AS-IS/TO-BE 좌우 + arrow (PROMOTED 구조) */
|
||||
.f29b__transform {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 4px;
|
||||
align-items: start;
|
||||
}
|
||||
.f29b__transform__list {
|
||||
/* .text-line 공통 contract 그대로. wrapper 만 grid 안의 cell 역할 */
|
||||
}
|
||||
.f29b__transform__arrow {
|
||||
align-self: center;
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.f29b__transform__arrow img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.f29b__footnote {
|
||||
font-size: var(--font-caption); /* 10px */
|
||||
line-height: var(--lh-caption);
|
||||
color: #333;
|
||||
padding-left: 14px;
|
||||
margin-top: 1px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.f29b__aster-small { font-size: 9px; }
|
||||
</style>
|
||||
|
||||
<div class="f29b" data-frame-id="1171281210" data-template-id="process_product_two_way">
|
||||
|
||||
|
||||
<div class="f29b__title">2. Process의 혁신과 Product의 변화</div>
|
||||
|
||||
|
||||
<div class="f29b__banners">
|
||||
<div class="f29b__banner f29b__banner--left">
|
||||
<div class="f29b__banner-text">과정(Process)의 혁신</div>
|
||||
</div>
|
||||
<div class="f29b__banner f29b__banner--right">
|
||||
<div class="f29b__banner-text">결과(Product)의 변화</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f29b__body">
|
||||
|
||||
<div class="f29b__grid">
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">Analogue 기반 업무의 Digital화</div>
|
||||
|
||||
<div class="transform-block">
|
||||
<div class="transform-block__header">
|
||||
<span class="transform-block__label--from">AS-IS</span>
|
||||
<span></span>
|
||||
<span class="transform-block__label--to">TO-BE</span>
|
||||
</div>
|
||||
<div class="transform-rows">
|
||||
|
||||
<div class="transform-row">
|
||||
<div class="transform-row__from">개념·문서·행정 절차 중심</div>
|
||||
<div class="transform-row__arrow">➜</div>
|
||||
<div class="transform-row__to">시각화된 목적물, 소통, 투명성 중심</div>
|
||||
</div>
|
||||
|
||||
<div class="transform-row">
|
||||
<div class="transform-row__from">2D 도면, 전문가, 규정</div>
|
||||
<div class="transform-row__arrow">➜</div>
|
||||
<div class="transform-row__to">3D 모델, 참여자, 실체</div>
|
||||
</div>
|
||||
|
||||
<div class="transform-row">
|
||||
<div class="transform-row__from">업무 구분(단절), 책임</div>
|
||||
<div class="transform-row__arrow">➜</div>
|
||||
<div class="transform-row__to">협업(융·복합), 창의성</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">Copy & Paste로 인해 하향 평준화된 기존 성과물의 품질 향상</div>
|
||||
<div class="text-line text-line--bullet">과거 수작업으로 시행하면서 발생하던 오류 등의 최소화</div><div class="text-line text-line--bullet">정확한 Data에 기반한 계획으로 고품질 성과물 도출</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">GIS + BIM의 연계</div>
|
||||
<div class="text-line text-line--bullet">지리·지형·지반 등 위치정보(GIS)와 3D모델(형상, 속성정보) 기반의 건설 정보를 포함하는 BIM의 연계를 통한 업무 프로세스의 혁신</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">Analogue 기반 도서 외 Digital 기반 정보물 추가</div>
|
||||
<div class="text-line text-line--bullet">기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">사용자 중심의 Solution 제공</div>
|
||||
<div class="text-line text-line--bullet">서로 다른 S/W로 작성되어 분절화된 Analogue 방식의 성과물과 정보물을 연계할 수 있는 설계·시공 Solution 제공</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">Solution을 활용한 업무 효율화</div>
|
||||
<div class="text-line text-line--bullet">Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성</div><div class="text-line text-line--bullet">건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slide-footer">
|
||||
<div class="slide-footer-text">DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 기대할 수 있다.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 280 KiB |
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: DX 실행 체계 구축 방안
|
||||
sidebar:
|
||||
order: 02
|
||||
---
|
||||
|
||||
## 1. DX 시행을 위한 필수 요건
|
||||
|
||||
<br/>
|
||||
|
||||
* **기술(디지털)**
|
||||
* **Digital 기술(S/W, H/W)과 업무 Process의 통합**
|
||||
* 기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행
|
||||
* 프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율
|
||||
* **분야별 전문 지식(설계, 시공, 유지관리 등) 보유**
|
||||
* 건설 전 단계에 대한 근본적인 이해와 지식 및 경험
|
||||
* 최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식
|
||||
|
||||
<br/>
|
||||
|
||||
* **사람(역량)**
|
||||
* **혁신적 사고방식과 창의적 문제 해결 능력**
|
||||
* 기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피
|
||||
* 디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시
|
||||
* **사용자 중심 사고와 DX 수행 경험**
|
||||
* 사용자의 요구와 기대를 충족시키는 설계 및 구현
|
||||
* 시행착오를 포함한 수행 경험과 사용자 경험(UX)을 반영한 해결 방안 제시
|
||||
|
||||
<br/>
|
||||
|
||||
* **자연(여건)**
|
||||
* **지속적인 투자 및 실행 의지**
|
||||
* 기술 도입 초기 단계에 필요한 인력·기간·비용 등의 대규모 투자
|
||||
* 기술 고도화를 위한 지속적인 개선 및 투자 체계 구축
|
||||
* 변화와 혁신을 통해 부가가치를 창출하려는 실행 의지와 추진력
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
## 2. Process의 혁신과 Product의 변화
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 과정(Process)의 혁신
|
||||
|
||||
* **Analogue 기반 업무의 Digital화**
|
||||
|
||||
| As-is [Analogue] | 구분 | To-be [Digital] |
|
||||
| :--- | :---: | :--- |
|
||||
| **개념·문서·행정 절차 중심** | ➠ | **시각화된 목적물, 소통, 투명성 중심** |
|
||||
| **2D 도면, 전문가, 규정** | ➠ | **3D 모델, 참여자, 실체** |
|
||||
| **업무 구분(단절), 책임** | ➠ | **협업(융·복합), 창의성** |
|
||||
|
||||
<br/>
|
||||
|
||||
* **GIS + BIM의 연계**
|
||||
* 지리·지형·지반 등 위치정보(GIS)와 3D모델(형상, 속성정보) 기반의 건설 정보를 포함하는 BIM의 연계를 통한 업무 프로세스의 혁신
|
||||
|
||||
<br/>
|
||||
|
||||
* **사용자 중심의 Solution 제공**
|
||||
* 서로 다른 S/W로 작성되어 분절화된 Analogue 방식의 성과물과 정보물을 연계할 수 있는 설계·시공 Solution 제공
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
### 2.2 결과(Product)의 변화
|
||||
|
||||
* **Copy & Paste로 인해 하향 평준화된 기존 성과물의 품질 향상**
|
||||
* 과거 수작업으로 시행하면서 발생하던 오류 등의 최소화
|
||||
* 정확한 Data에 기반한 계획으로 고품질 성과물 도출
|
||||
|
||||
<br/>
|
||||
|
||||
* **Analogue 기반 도서 외 Digital 기반 정보물 추가**
|
||||
* 기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가
|
||||
|
||||
<br/>
|
||||
|
||||
* **Solution을 활용한 업무 효율화**
|
||||
* Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성
|
||||
* 건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* **DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 기대할 수 있다.**
|
||||
:::
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"step_num": 1,
|
||||
"step_name": "mdx_upload",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"samples\\mdx_batch\\03.mdx"
|
||||
],
|
||||
"output": [
|
||||
"step01_mdx_upload.json",
|
||||
"step01_mdx_source.md"
|
||||
],
|
||||
"note": "MDX 원본 그대로 step01_mdx_source.md 에 복사.",
|
||||
"data": {
|
||||
"mdx_path": "samples\\mdx_batch\\03.mdx",
|
||||
"run_id": "mdx03_f29_fix_check",
|
||||
"run_dir": "d:\\ad-hoc\\kei\\design_agent\\data\\runs\\mdx03_f29_fix_check\\phase_z2",
|
||||
"mdx_source_size_bytes": 3213,
|
||||
"mdx_source_lines": 89
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"step_num": 2,
|
||||
"step_name": "normalized",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step01_mdx_source.md"
|
||||
],
|
||||
"output": [
|
||||
"step02_normalized.json"
|
||||
],
|
||||
"note": "parse_mdx 결과: title / sections / footer 분리 + raw_content 보존. heading tree 미생성, orphan / details 감지 미완 (Step 2 ⚠ partial — 별 axis). orphans / details 필드는 schema lock — 빈 배열이라도 'detection 미수행' marker.",
|
||||
"data": {
|
||||
"slide_title": "DX 실행 체계 구축 방안",
|
||||
"slide_footer": "DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 기대할 수 있다.",
|
||||
"sections_count": 2,
|
||||
"sections": [
|
||||
{
|
||||
"section_id": "03-1",
|
||||
"section_num": 1,
|
||||
"title": "1. DX 시행을 위한 필수 요건",
|
||||
"raw_content_length": 660,
|
||||
"raw_content": "<br/>\n\n* **기술(디지털)**\n * **Digital 기술(S/W, H/W)과 업무 Process의 통합**\n * 기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행\n * 프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율\n * **분야별 전문 지식(설계, 시공, 유지관리 등) 보유**\n * 건설 전 단계에 대한 근본적인 이해와 지식 및 경험\n * 최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식\n\n<br/>\n\n* **사람(역량)**\n * **혁신적 사고방식과 창의적 문제 해결 능력**\n * 기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피\n * 디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시\n * **사용자 중심 사고와 DX 수행 경험**\n * 사용자의 요구와 기대를 충족시키는 설계 및 구현\n * 시행착오를 포함한 수행 경험과 사용자 경험(UX)을 반영한 해결 방안 제시\n\n<br/>\n\n* **자연(여건)**\n * **지속적인 투자 및 실행 의지**\n * 기술 도입 초기 단계에 필요한 인력·기간·비용 등의 대규모 투자\n * 기술 고도화를 위한 지속적인 개선 및 투자 체계 구축\n * 변화와 혁신을 통해 부가가치를 창출하려는 실행 의지와 추진력\n\n<br/>\n---"
|
||||
},
|
||||
{
|
||||
"section_id": "03-2",
|
||||
"section_num": 2,
|
||||
"title": "2. Process의 혁신과 Product의 변화",
|
||||
"raw_content_length": 924,
|
||||
"raw_content": "<br/>\n\n### 2.1 과정(Process)의 혁신\n\n* **Analogue 기반 업무의 Digital화**\n\n | As-is [Analogue] | 구분 | To-be [Digital] |\n | :--- | :---: | :--- |\n | **개념·문서·행정 절차 중심** | ➠ | **시각화된 목적물, 소통, 투명성 중심** |\n | **2D 도면, 전문가, 규정** | ➠ | **3D 모델, 참여자, 실체** |\n | **업무 구분(단절), 책임** | ➠ | **협업(융·복합), 창의성** |\n\n<br/>\n\n* **GIS + BIM의 연계**\n * 지리·지형·지반 등 위치정보(GIS)와 3D모델(형상, 속성정보) 기반의 건설 정보를 포함하는 BIM의 연계를 통한 업무 프로세스의 혁신\n\n<br/>\n\n* **사용자 중심의 Solution 제공**\n * 서로 다른 S/W로 작성되어 분절화된 Analogue 방식의 성과물과 정보물을 연계할 수 있는 설계·시공 Solution 제공\n\n<br/>\n<br/>\n\n### 2.2 결과(Product)의 변화\n\n* **Copy & Paste로 인해 하향 평준화된 기존 성과물의 품질 향상**\n * 과거 수작업으로 시행하면서 발생하던 오류 등의 최소화\n * 정확한 Data에 기반한 계획으로 고품질 성과물 도출\n\n<br/>\n\n* **Analogue 기반 도서 외 Digital 기반 정보물 추가**\n * 기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가\n\n<br/>\n\n* **Solution을 활용한 업무 효율화**\n * Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성\n * 건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리\n\n<br/>\n---"
|
||||
}
|
||||
],
|
||||
"orphans": [],
|
||||
"details": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"step_num": 5,
|
||||
"step_name": "v4_evidence",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step02_normalized.json",
|
||||
"tests/matching/v4_full32_result.yaml"
|
||||
],
|
||||
"output": [
|
||||
"step05_v4_evidence.json"
|
||||
],
|
||||
"note": "V4 non-reject max-6 후보 list (Step 9 application_plan input). raw 32 entry 는 tests/matching/v4_full32_result.yaml 에 영속. candidate_status='ok' = 후보 1개 이상 / 'no_non_reject_v4_candidate' = 0개 (Step 9 fallback path 입력). Step 6 plan_composition() 은 lookup_v4_match() (rank-1) 그대로 사용 — backward compat (Step 6-A axis 까지).",
|
||||
"data": {
|
||||
"v4_source": "tests\\matching\\v4_full32_result.yaml",
|
||||
"aligned_section_ids": [
|
||||
"03-1",
|
||||
"03-2"
|
||||
],
|
||||
"evidence_per_section": [
|
||||
{
|
||||
"section_id": "03-1",
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"confidence": 0.9268,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"frame_number": 20,
|
||||
"confidence": 0.8413,
|
||||
"label": "light_edit"
|
||||
}
|
||||
],
|
||||
"candidate_status": "ok"
|
||||
},
|
||||
{
|
||||
"section_id": "03-2",
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.9198,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"frame_number": 27,
|
||||
"confidence": 0.6278,
|
||||
"label": "restructure"
|
||||
}
|
||||
],
|
||||
"candidate_status": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"step_num": 6,
|
||||
"step_name": "composition_plan",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step02_normalized.json",
|
||||
"step05_v4_evidence.json"
|
||||
],
|
||||
"output": [
|
||||
"step06_composition_plan.json"
|
||||
],
|
||||
"note": "composition v0 count-based — sections → candidates → score → greedy select. Step 6-A (사용자 lock 2026-05-08): selected_units[i].v4_candidates 추가 (non-reject max-6 후보 list, candidates[0] = 단일 frame_* 와 일관). logic 무변 — runtime 결과 동일. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"selected_units_count": 2,
|
||||
"layout_preset_decided": "horizontal-2",
|
||||
"candidates_summary": [
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "three_parallel_requirements",
|
||||
"label": "use_as_is",
|
||||
"phase_z_status": "matched_zone",
|
||||
"score": 0.9268,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 3,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 3,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "process_product_two_way",
|
||||
"label": "use_as_is",
|
||||
"phase_z_status": "matched_zone",
|
||||
"score": 0.9198,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"candidates_total": 2,
|
||||
"candidates_viable_auto": 2,
|
||||
"selected_units": [
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"frame_template_id": "three_parallel_requirements",
|
||||
"label": "use_as_is",
|
||||
"score": 0.9268,
|
||||
"phase_z_status": "matched_zone",
|
||||
"rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 3,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 3,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.9268,
|
||||
"confidence": 0.9268,
|
||||
"label": "use_as_is",
|
||||
"label_weight": 1.0,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"notes": [],
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"confidence": 0.9268,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"frame_number": 20,
|
||||
"confidence": 0.8413,
|
||||
"label": "light_edit"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source_section_ids": [
|
||||
"03-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"frame_template_id": "process_product_two_way",
|
||||
"label": "use_as_is",
|
||||
"score": 0.9198,
|
||||
"phase_z_status": "matched_zone",
|
||||
"rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.9198,
|
||||
"confidence": 0.9198,
|
||||
"label": "use_as_is",
|
||||
"label_weight": 1.0,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"notes": [],
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.9198,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"frame_number": 27,
|
||||
"confidence": 0.6278,
|
||||
"label": "restructure"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"step_num": 7,
|
||||
"step_name": "layout",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step06_composition_plan.json"
|
||||
],
|
||||
"output": [
|
||||
"step07_layout.json",
|
||||
"step07_selected_layout.html"
|
||||
],
|
||||
"note": "count-based v0 — 들여쓰기 / 정렬 미세 layout 미구현 (Step 7 ⚠ partial). Step 7-conn (사용자 lock 2026-05-08): unit_count → layout_candidates list (select_layout_candidates) artifact 기록. default 결정 자체는 무변 — candidates[0] 가 default (default_selection: true), 나머지는 alternative. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_css": {
|
||||
"areas": "\"top\" \"bottom\"",
|
||||
"cols": "1fr",
|
||||
"rows": "228px 343px",
|
||||
"heights_px": [
|
||||
228,
|
||||
343
|
||||
],
|
||||
"ratios": [
|
||||
0.39,
|
||||
0.586
|
||||
],
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"dynamic_rows": true,
|
||||
"raw_zone_layout": {
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"slide_body_height": 585,
|
||||
"gap": 14,
|
||||
"available_after_gap": 571,
|
||||
"min_heights_px": [
|
||||
228,
|
||||
342
|
||||
],
|
||||
"min_scaled": true,
|
||||
"total_min_height": 570,
|
||||
"remaining_after_min": 1,
|
||||
"content_weights": [
|
||||
{
|
||||
"position": "top",
|
||||
"template_id": "three_parallel_requirements",
|
||||
"score": 4.425
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"template_id": "process_product_two_way",
|
||||
"score": 7.305
|
||||
}
|
||||
],
|
||||
"weight_shares": [
|
||||
0.377,
|
||||
0.623
|
||||
],
|
||||
"extras_px": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"heights_px": [
|
||||
228,
|
||||
343
|
||||
],
|
||||
"ratios": [
|
||||
0.39,
|
||||
0.586
|
||||
]
|
||||
}
|
||||
},
|
||||
"zones_count": 2,
|
||||
"unit_count": 2,
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"step_num": 8,
|
||||
"step_name": "zone_region_ratios",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step07_layout.json",
|
||||
"step10_frame_contract.json"
|
||||
],
|
||||
"output": [
|
||||
"step08_zone_region_ratios.json",
|
||||
"step08_zone_content_placement.html"
|
||||
],
|
||||
"note": "계획값 only. 실측값은 Step 14 (visual_check). zone-level 만 dynamic, region-level (sub_zone 안 sections) 은 균등 분배 (1/1/1). F29 처럼 비균등 콘텐츠 (transform_table + plain) 는 첫 cell 부족 가능 — Step 8 ⚠ partial. Step 8-conn (사용자 lock 2026-05-08): per_zone_plan[i] 에 region_layout_candidates + display_strategy_candidates 추가 (passive 기록). placeholder signals = text-only / region_count=1 (Step 3/4 부재 종속). 실제 content_object 신호 활성화는 별 axis. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"zone_heights_px_planned": [
|
||||
228,
|
||||
343
|
||||
],
|
||||
"zone_ratios_planned": [
|
||||
0.39,
|
||||
0.586
|
||||
],
|
||||
"per_zone_plan": [
|
||||
{
|
||||
"position": "top",
|
||||
"zone_height_px_planned": 228,
|
||||
"zone_ratio_planned": 0.39,
|
||||
"min_height_px": 230,
|
||||
"frame_cardinality_strict": 3,
|
||||
"sub_zones_planned": [
|
||||
{
|
||||
"id": "pillar_1",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": 1,
|
||||
"partial_target_path": ".f13b__cols > .f13b__col:nth-child(1)"
|
||||
},
|
||||
{
|
||||
"id": "pillar_2",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": 1,
|
||||
"partial_target_path": ".f13b__cols > .f13b__col:nth-child(2)"
|
||||
},
|
||||
{
|
||||
"id": "pillar_3",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": 1,
|
||||
"partial_target_path": ".f13b__cols > .f13b__col:nth-child(3)"
|
||||
}
|
||||
],
|
||||
"child_distribution_note": "현재 child zone (sub_zones 안 sections) 균등 분배 — Step 8 region-level ratio ⚠ partial.",
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"zone_height_px_planned": 343,
|
||||
"zone_ratio_planned": 0.586,
|
||||
"min_height_px": 345,
|
||||
"frame_cardinality_strict": 2,
|
||||
"sub_zones_planned": [
|
||||
{
|
||||
"id": "process_column",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block",
|
||||
"transform_table"
|
||||
],
|
||||
"cardinality_strict": 3,
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--left"
|
||||
},
|
||||
{
|
||||
"id": "product_column",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": 3,
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--right"
|
||||
}
|
||||
],
|
||||
"child_distribution_note": "현재 child zone (sub_zones 안 sections) 균등 분배 — Step 8 region-level ratio ⚠ partial.",
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
]
|
||||
}
|
||||
],
|
||||
"step8_conn_placeholder_signals": {
|
||||
"region_count": 1,
|
||||
"content_type": "text_block",
|
||||
"flow_type": null,
|
||||
"role_pattern": null,
|
||||
"ratio_asymmetric": false,
|
||||
"has_visual_element": false,
|
||||
"details_presence": false,
|
||||
"large_table": false,
|
||||
"long_text": false,
|
||||
"fits_in_region": null,
|
||||
"_note": "Step 3/4 부재 종속 placeholder — 모든 zone 을 text-only / region_count=1 로 가정. 실제 content_object 신호 활성화는 별 axis."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"step_num": 9,
|
||||
"step_name": "application_plan",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step05_v4_evidence.json",
|
||||
"step06_composition_plan.json",
|
||||
"step07_layout.json",
|
||||
"step08_zone_region_ratios.json"
|
||||
],
|
||||
"output": [
|
||||
"step09_application_plan.json",
|
||||
"step09_application_plan.html"
|
||||
],
|
||||
"note": "Step 9 v0 passive application_plan trace (사용자 lock 2026-05-08). V4 후보 + layout / region / display 후보 + V4 label → application_mode 변환을 side-by-side 로 기록. v0 invariant 5 가지 (status.md §4) 만족. Step 6 의 default 결정 그대로 (current_default_candidate). auto decision 은 Step 9 v1 (별 axis). region/display 후보는 Step 8-conn 의 placeholder signal 종속 (Step 3/4 부재).",
|
||||
"data": {
|
||||
"units": [
|
||||
{
|
||||
"unit_id": "03-1",
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
],
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
],
|
||||
"candidate_status": "ok",
|
||||
"application_status": "ok",
|
||||
"current_default_candidate": "three_parallel_requirements",
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"confidence": 0.9268,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"frame_number": 20,
|
||||
"confidence": 0.8413,
|
||||
"label": "light_edit"
|
||||
}
|
||||
],
|
||||
"application_candidates": [
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"v4_label": "use_as_is",
|
||||
"application_mode": "direct_insert",
|
||||
"auto_applicable": true,
|
||||
"required_changes": [],
|
||||
"delegated_to": "step10_contract_check"
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"v4_label": "light_edit",
|
||||
"application_mode": "same_frame_with_adjustment",
|
||||
"auto_applicable": true,
|
||||
"required_changes": [],
|
||||
"delegated_to": "step10_contract_check"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"unit_id": "03-2",
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
],
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
],
|
||||
"candidate_status": "ok",
|
||||
"application_status": "ok",
|
||||
"current_default_candidate": "process_product_two_way",
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.9198,
|
||||
"label": "use_as_is"
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"frame_number": 27,
|
||||
"confidence": 0.6278,
|
||||
"label": "restructure"
|
||||
}
|
||||
],
|
||||
"application_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"v4_label": "use_as_is",
|
||||
"application_mode": "direct_insert",
|
||||
"auto_applicable": true,
|
||||
"required_changes": [],
|
||||
"delegated_to": "step10_contract_check"
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"v4_label": "restructure",
|
||||
"application_mode": "layout_or_region_change",
|
||||
"auto_applicable": false,
|
||||
"required_changes": [],
|
||||
"delegated_to": "human_review"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"candidate_status_summary": {
|
||||
"units_with_no_v4_candidate": []
|
||||
},
|
||||
"v0_lock_note": "Step 9 v0 passive (사용자 lock 2026-05-08). Step 6 default 그대로 사용 — runtime 결과 byte-동일. auto decision / scoring 은 Step 9 v1 (별 axis)."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"step_num": 20,
|
||||
"step_name": "slide_status",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step14_visual_check.json",
|
||||
"step17_retry_trace.json",
|
||||
"step19_next_action.json"
|
||||
],
|
||||
"output": [
|
||||
"step20_slide_status.json",
|
||||
"step20_final_status.html"
|
||||
],
|
||||
"note": "자동 파이프라인 최종 결과 보고. overall = PASS/RENDERED_WITH_VISUAL_REGRESSION/PARTIAL_COVERAGE 등.",
|
||||
"data": {
|
||||
"rendered": true,
|
||||
"visual_check_passed": true,
|
||||
"full_mdx_coverage": true,
|
||||
"aligned_section_ids": [
|
||||
"03-1",
|
||||
"03-2"
|
||||
],
|
||||
"covered_section_ids": [
|
||||
"03-1",
|
||||
"03-2"
|
||||
],
|
||||
"filtered_section_ids": [],
|
||||
"filtered_section_reasons": [],
|
||||
"visual_fail_reasons": [],
|
||||
"adapter_needed_count": 0,
|
||||
"adapter_needed_units": [],
|
||||
"content_truncated_count": 0,
|
||||
"content_truncated_units": [],
|
||||
"overall": "PASS",
|
||||
"note": "자동 파이프라인 결과 보고. review/UI 개념 X. final.html 파일명 != PASS 의미. overall == PASS 는 visual OK + full coverage + adapter_needed=0 일 때만. adapter_needed_count > 0 = mapper 가 contract 와 안 맞아 자동 렌더 못 한 zone 존재. content_truncated_count > 0 = builder 가 truncate 한 zone 존재 (rendered 됐지만 일부 콘텐츠 손실)."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,992 @@
|
||||
<!-- Phase Z-2 MVP-1.5b — single slide + Type B layout (top/bottom zones).
|
||||
원래 Phase Z 설계 복귀: MDX 1 = slide 1, layout preset = zone 분할, frame-derived block ⊂ zone. -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=1280">
|
||||
<title>DX 지연 요인</title>
|
||||
<style>
|
||||
/* ── existing tokens (inlined) ── */
|
||||
/* === typography.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Typography Tokens v1
|
||||
══════════════════════════════════════
|
||||
슬라이드 전체의 공통 글자 위계.
|
||||
모든 블록은 이 토큰을 참조한다.
|
||||
직접값(font-size: 11px) 금지 → var(--font-body) 사용.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── 1층: Global Hierarchy ── */
|
||||
|
||||
/* 대목차: 슬라이드 상단 제목 */
|
||||
--font-slide-title: 22px;
|
||||
--weight-slide-title: 700;
|
||||
--lh-slide-title: 1.4;
|
||||
|
||||
/* 중목차: zone 제목 */
|
||||
--font-zone-title: 16px;
|
||||
--weight-zone-title: 700;
|
||||
--lh-zone-title: 1.4;
|
||||
|
||||
/* 소목차: 블록 내 소제목, 카드 제목 */
|
||||
--font-sub-title: 12px;
|
||||
--weight-sub-title: 700;
|
||||
--lh-sub-title: 1.45;
|
||||
|
||||
/* 본문: 블릿, 설명 텍스트 */
|
||||
--font-body: 10px;
|
||||
--weight-body: 500;
|
||||
--lh-body: 1.55;
|
||||
|
||||
/* 캡션: 각주, 출처, 보조 텍스트 */
|
||||
--font-caption: 10px;
|
||||
--weight-caption: 400;
|
||||
--lh-caption: 1.4;
|
||||
|
||||
/* footer: 핵심 인사이트 pill */
|
||||
--font-footer: 20px;
|
||||
--weight-footer: 700;
|
||||
--lh-footer: 1.2;
|
||||
|
||||
/* ── 2층: Component Semantic Tokens ── */
|
||||
/* 가까운 위계에서 기본값, 필요시 override */
|
||||
|
||||
/* 본문 강조 (인라인 heading) */
|
||||
--font-body-strong: var(--font-body);
|
||||
--weight-body-strong: 600;
|
||||
|
||||
/* 자세히보기 링크 */
|
||||
--font-detail-link: var(--font-caption);
|
||||
--weight-detail-link: 500;
|
||||
|
||||
/* pill/badge 라벨 */
|
||||
--font-pill-label: var(--font-sub-title);
|
||||
--weight-pill-label: 700;
|
||||
|
||||
/* 표 헤더 */
|
||||
--font-table-header: var(--font-sub-title);
|
||||
--weight-table-header: 700;
|
||||
|
||||
/* 표 셀 */
|
||||
--font-table-cell: var(--font-body);
|
||||
--weight-table-cell: 400;
|
||||
|
||||
/* 비교 뱃지 */
|
||||
--font-compare-badge: var(--font-sub-title);
|
||||
--weight-compare-badge: 700;
|
||||
|
||||
/* 강조 인용 */
|
||||
--font-callout: var(--font-sub-title);
|
||||
--weight-callout: 700;
|
||||
|
||||
/* 상단 라벨 (overline) */
|
||||
--font-overline: var(--font-caption);
|
||||
--weight-overline: 600;
|
||||
--ls-overline: 0.05em;
|
||||
}
|
||||
|
||||
|
||||
/* === spacing.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Spacing Tokens v1
|
||||
══════════════════════════════════════
|
||||
여백/간격/들여쓰기 공통 기준.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ── 기본 스케일 ── */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 24px;
|
||||
|
||||
/* ── slide-base 레이아웃 ── */
|
||||
--slide-padding-x: 40px; /* 본문 좌우 여백 */
|
||||
--slide-title-left: 52px; /* 대목차 시작 */
|
||||
--slide-title-top: 22px; /* 대목차 위치 */
|
||||
--slide-divider-top: 58px; /* 구분선 위치 (slide_base.html 와 일치 2026-05-07) */
|
||||
--slide-body-top: 76px; /* 본문 시작 (사용자 직설: divider-body 16px) */
|
||||
--slide-body-height: 585px; /* 본문 높이 (사용자 직설: body-footer 10px) */
|
||||
--slide-footer-bottom: 8px; /* footer 하단 여백 (slide_base.html 와 일치) */
|
||||
--slide-footer-height: 41px; /* footer 높이 */
|
||||
|
||||
/* ── zone ── */
|
||||
--zone-gap: 8px; /* zone 간 간격 */
|
||||
--zone-padding-left: 12px; /* zone 안쪽 좌측 여백 */
|
||||
--zone-padding-right: 8px; /* zone 안쪽 우측 여백 */
|
||||
--zone-title-mb: 8px; /* zone 제목 아래 여백 */
|
||||
|
||||
/* ── 블록 공통 ── */
|
||||
--card-padding: 16px; /* 카드 내부 여백 */
|
||||
--card-padding-sm: 8px; /* 작은 카드 내부 여백 */
|
||||
--card-gap: 12px; /* 카드 간 간격 */
|
||||
--card-radius: 6px; /* 카드 모서리 */
|
||||
|
||||
/* ── 블릿/텍스트 ── */
|
||||
--bullet-indent: 14px; /* 불릿 hanging indent */
|
||||
--bullet-gap: 2px; /* 불릿 항목 간 간격 */
|
||||
--heading-gap: 4px; /* 소제목 아래 간격 */
|
||||
--section-gap: 10px; /* 섹션 간 간격 */
|
||||
|
||||
/* ── flex/grid 기본 ── */
|
||||
--flex-gap: 12px; /* 기본 flex gap */
|
||||
--flex-gap-sm: 6px; /* 좁은 flex gap */
|
||||
--grid-gap: 16px; /* 기본 grid gap */
|
||||
}
|
||||
|
||||
|
||||
/* === colors.css === */
|
||||
/* ══════════════════════════════════════
|
||||
Color Tokens v1
|
||||
══════════════════════════════════════
|
||||
2층 구조: 공통 테마색 + 블록 의미색.
|
||||
══════════════════════════════════════ */
|
||||
|
||||
:root {
|
||||
/* ══ 1층: 공통 테마색 ══ */
|
||||
/* 모든 블록이 공유하는 기본 팔레트 */
|
||||
|
||||
/* 텍스트 */
|
||||
--color-title: #1e293b; /* 제목 (진한 남색) */
|
||||
--color-zone-title: #1a365d; /* zone 중목차 제목 */
|
||||
--color-body: #475569; /* 본문 */
|
||||
--color-body-strong: #1e293b; /* 본문 강조 */
|
||||
--color-caption: #94a3b8; /* 캡션, 보조 */
|
||||
--color-muted: #64748b; /* 약한 텍스트 */
|
||||
|
||||
/* 배경 */
|
||||
--color-bg: #ffffff; /* 슬라이드 배경 */
|
||||
--color-bg-subtle: #f8fafc; /* 카드/영역 배경 */
|
||||
--color-bg-page: #e8ecf0; /* 페이지 배경 (슬라이드 바깥) */
|
||||
|
||||
/* 보더 */
|
||||
--color-border: #e2e8f0; /* 기본 보더 */
|
||||
--color-border-strong: #cbd5e1; /* 강한 보더 */
|
||||
|
||||
/* 강조 */
|
||||
--color-accent: #2563eb; /* 링크, 강조 (파랑) */
|
||||
--color-accent-hover: #eff6ff; /* 링크 hover 배경 */
|
||||
--color-danger: #dc2626; /* 경고, 에러 */
|
||||
|
||||
/* slide-base 전용 */
|
||||
--color-slide-title-from: #296b55; /* 대목차 gradient 시작 */
|
||||
--color-slide-title-to: #123328; /* 대목차 gradient 끝 */
|
||||
--color-footer-bg-from: #3b3523; /* footer gradient 시작 */
|
||||
--color-footer-bg-mid: #263a2a; /* footer gradient 중간 */
|
||||
--color-footer-bg-to: #113f31; /* footer gradient 끝 */
|
||||
--color-footer-text: #ffffff; /* footer 텍스트 */
|
||||
--color-footer-accent: #ffee33; /* footer 강조 (em) */
|
||||
|
||||
/* ══ 2층: 블록 의미색 ══ */
|
||||
/* 특정 블록 유형에서 의미를 가지는 색 */
|
||||
|
||||
/* 3열 비교 (prerequisites-3col) */
|
||||
--color-col-1-from: #0D78D0; /* 1열 gradient 시작 */
|
||||
--color-col-1-to: #023056; /* 1열 gradient 끝 */
|
||||
--color-col-2-from: #FF9A23; /* 2열 gradient 시작 */
|
||||
--color-col-2-to: #CC5200; /* 2열 gradient 끝 */
|
||||
--color-col-3-from: #39BE49; /* 3열 gradient 시작 */
|
||||
--color-col-3-to: #23742C; /* 3열 gradient 끝 */
|
||||
|
||||
/* 비교 블록 (compare) */
|
||||
--color-compare-left: #5c3714; /* 좌측 (AS-IS 계열) */
|
||||
--color-compare-right: #285b4a; /* 우측 (TO-BE 계열) */
|
||||
--color-compare-badge: #ae3607; /* VS 뱃지 */
|
||||
|
||||
/* 다크 카드 (direct render) */
|
||||
--color-dark-card-1: #1a365d; /* 카드 1 배경 */
|
||||
--color-dark-card-2: #1e3a2f; /* 카드 2 배경 */
|
||||
--color-dark-card-3: #3b1f2b; /* 카드 3 배경 */
|
||||
--color-dark-card-title: #fbbf24; /* 다크카드 제목 (금색) */
|
||||
--color-dark-card-body: #e2e8f0; /* 다크카드 본문 */
|
||||
|
||||
/* 표 */
|
||||
--color-table-header-bg: #64748b; /* 표 헤더 배경 */
|
||||
--color-table-header-text: #ffffff; /* 표 헤더 텍스트 */
|
||||
|
||||
/* 블록 제목 gradient (Figma 원본) */
|
||||
--color-block-title-from: #CC5200; /* 주황 gradient 시작 */
|
||||
--color-block-title-to: #883700; /* 주황 gradient 끝 */
|
||||
|
||||
/* pill */
|
||||
--color-pill-bg: #1e293b; /* pill 배경 */
|
||||
--color-pill-text: #ffffff; /* pill 텍스트 */
|
||||
}
|
||||
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
background: #e8ecf0;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
min-height: 100vh;
|
||||
word-break: keep-all;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* ── 16:9 슬라이드 (single, 1280×720) ── */
|
||||
.slide {
|
||||
width: 1280px; height: 720px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.slide-bg {
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
.slide-title {
|
||||
position: absolute;
|
||||
left: 52px; top: 22px;
|
||||
width: calc(100% - 104px);
|
||||
font-weight: 700;
|
||||
font-size: var(--font-slide-title);
|
||||
line-height: var(--lh-slide-title);
|
||||
background-image: linear-gradient(180deg, #296b55 0%, #123328 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 2px #322c1e;
|
||||
z-index: 2;
|
||||
}
|
||||
.slide-divider {
|
||||
position: absolute;
|
||||
left: 50px; top: 58px;
|
||||
width: calc(100% - 100px);
|
||||
height: 2px;
|
||||
background: #cbd5e1;
|
||||
z-index: 2;
|
||||
}
|
||||
.slide-body {
|
||||
position: absolute;
|
||||
left: 50px; top: 76px;
|
||||
width: calc(100% - 100px); /* 1180 */
|
||||
height: 585px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Layout preset (composition planner v0 — 8 vocabulary) ──
|
||||
단일 .layout-{preset} 클래스에 grid CSS 주입.
|
||||
layout_css = pipeline build_layout_css() 결과 :
|
||||
- horizontal-2 : rows = dynamic px (content_weight + frame_min_height)
|
||||
- 그 외 : rows / cols = fr default (LAYOUT_PRESETS)
|
||||
positions (single / top / bottom / left / right / top-left / ... / grid-2x2 9개)
|
||||
은 .zone 의 inline grid-area 로 지정 — preset CSS 는 areas 만 정의. */
|
||||
.layout-horizontal-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 211px 360px;
|
||||
grid-template-areas: "top" "bottom";
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Zone — block 가용 공간 최대화. 프레임 자체 styling 이 boundary 역할 ── */
|
||||
.zone {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Frame-family text layout contract (shared, reusable) ──
|
||||
feedback-1 (mvp1.5b_test7): visible improvement 강화.
|
||||
Stronger hanging indent + breathing line spacing + visible hierarchy. */
|
||||
.text-line {
|
||||
font-size: var(--font-body); /* 11px */
|
||||
font-weight: var(--weight-body);
|
||||
line-height: 1.6; /* breathing room */
|
||||
margin: 0;
|
||||
}
|
||||
.text-line + .text-line {
|
||||
margin-top: 4px; /* visible gap between lines (was 2px) */
|
||||
}
|
||||
|
||||
/* bullet line — hanging indent via padding + absolute marker (안정적) */
|
||||
.text-line--bullet {
|
||||
padding-left: 14px;
|
||||
position: relative;
|
||||
}
|
||||
.text-line--bullet::before {
|
||||
content: "•"; /* visible bullet 마커 */
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 0;
|
||||
font-weight: 700;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
/* indent levels — visible nested hierarchy */
|
||||
.text-line--indent-1 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
.text-line--indent-1::before {
|
||||
content: "◦"; /* sub-bullet 다른 마커 */
|
||||
left: 18px;
|
||||
}
|
||||
.text-line--indent-2 {
|
||||
padding-left: 46px;
|
||||
}
|
||||
.text-line--indent-2::before {
|
||||
content: "▪";
|
||||
left: 34px;
|
||||
}
|
||||
/* body line — 마커 없음 */
|
||||
.text-line--body {
|
||||
padding-left: 0;
|
||||
}
|
||||
.text-line strong { color: #000; font-weight: 700; }
|
||||
|
||||
/* ── Transform-block family (frame 29 AS-IS/TO-BE dedicated comparison component) ──
|
||||
feedback-3: bullet rows 가 아닌 *전용 비교 component*. Header (AS-IS / TO-BE) + paired rows.
|
||||
color-coded cells, left-border accent → 단번에 *비교 구조* 인지. */
|
||||
.transform-block {
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px 6px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.transform-block__header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 22px 1fr;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px dashed rgba(0,0,0,0.12);
|
||||
font-size: 9px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.transform-block__label--from { color: #6b5444; }
|
||||
.transform-block__label--to { color: #2563eb; text-align: left; }
|
||||
.transform-rows {
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.transform-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 22px 1fr;
|
||||
gap: 6px;
|
||||
align-items: stretch;
|
||||
font-size: var(--font-body);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.transform-row__from {
|
||||
padding: 3px 8px;
|
||||
background: linear-gradient(90deg, rgba(140,120,90,0.16), rgba(140,120,90,0.08));
|
||||
border-left: 2px solid #b8a98a;
|
||||
border-radius: 2px;
|
||||
color: #4b3f30;
|
||||
font-weight: 500;
|
||||
}
|
||||
.transform-row__arrow {
|
||||
text-align: center;
|
||||
color: #2563eb;
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
align-self: center;
|
||||
}
|
||||
.transform-row__to {
|
||||
padding: 3px 8px;
|
||||
background: linear-gradient(90deg, rgba(37,99,235,0.10), rgba(37,99,235,0.18));
|
||||
border-left: 2px solid #2563eb;
|
||||
border-radius: 2px;
|
||||
color: #1e40af;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Footer pill — feedback-3: body 와 weight balance, 살짝 가벼워짐 ── */
|
||||
.slide-footer {
|
||||
position: absolute;
|
||||
left: 50px; bottom: 8px;
|
||||
width: calc(100% - 100px); /* front 기준 (body 와 정렬) */
|
||||
height: 41px; /* front 기준 */
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(90deg, #3b3523 5%, #263a2a 50%, #113f31 95%);
|
||||
opacity: 0.92; /* slightly lighter */
|
||||
}
|
||||
.slide-footer-text {
|
||||
position: relative; z-index: 1;
|
||||
font-size: 15px; /* was 20px (--font-footer) */
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 3px rgba(0,0,0,.4);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.slide-footer-text em { color: #fe3; font-style: normal; }
|
||||
|
||||
.phase-z2-marker {
|
||||
position: absolute;
|
||||
top: 4px; right: 8px;
|
||||
font-size: 9px; color: #94a3b8;
|
||||
font-family: monospace;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide" data-page="1">
|
||||
<div class="slide-bg"></div>
|
||||
<div class="phase-z2-marker">phase_z2 / mvp-1.5b / horizontal-2 / single slide</div>
|
||||
<div class="slide-title">DX 지연 요인</div>
|
||||
<div class="slide-divider"></div>
|
||||
<div class="slide-body">
|
||||
<div class="layout-horizontal-2">
|
||||
|
||||
<div class="zone" data-zone-position="top" data-template-id="pre_construction_model_info_stacked" style="grid-area: top;">
|
||||
<!-- Phase Z-2 신규 frame partial — Frame 9 (1171281180) pre_construction_model_info_stacked.
|
||||
2026-05-14 — V4 04-1 top rank-1 매칭 (의미 conf=0.722) + structure cardinality 5 일치.
|
||||
사용자 룰 부합 : Figma visual (계단 pill / 5 색상 / vertical label) 유지 + 콘텐츠 cardinality
|
||||
에 맞춰 보완 (pill 안 1 라인 → multi-line: pill_N_label + pill_N_body). -->
|
||||
|
||||
|
||||
<style>
|
||||
.f9b {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 6px;
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
word-break: keep-all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 2026-05-14 — title 스타일 통일 (frame 29 process_product_two_way 와 동일 패턴) :
|
||||
배경 box + box-shadow 제거, gradient 텍스트 만 유지. 사용자 룰 Q1. */
|
||||
.f9b__title {
|
||||
font-size: var(--font-zone-title);
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-zone-title);
|
||||
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
flex-shrink: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.f9b__body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 좌측 vertical label — 콘텐츠 있을 때만 표시 */
|
||||
.f9b__vlabel {
|
||||
flex: 0 0 auto;
|
||||
width: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-sub-title);
|
||||
line-height: 1.2;
|
||||
color: #144838;
|
||||
text-align: center;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
text-shadow: 0 2px 2px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 2026-05-14 — pill 배치 vertical → horizontal (grid). 5 pills 를 가로로 나열 →
|
||||
우측 공란 활용 + 세로 공간 절약. auto-fit + minmax 으로 N 동적 (4 / 5 / 6 모두 자동). */
|
||||
.f9b__pill-rows {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 2026-05-14 — bullet 줄간격 1/3 으로 압축 + 다른 간격 (padding, label↔body) 살짝 늘림. */
|
||||
.f9b__pill {
|
||||
min-height: 0;
|
||||
background: rgba(255,255,255,0.7);
|
||||
border-radius: 10px;
|
||||
border-bottom: 3px solid;
|
||||
box-shadow: 1px 2px 4px rgba(0,0,0,0.15);
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
align-self: start;
|
||||
}
|
||||
/* Figma frame 9 의 5 색상 — nth-child cycle. N>5 면 default border-bottom 색 (회색). */
|
||||
.f9b__pill:nth-child(5n+1) { border-bottom-color: #fb5915; }
|
||||
.f9b__pill:nth-child(5n+2) { border-bottom-color: #e79000; }
|
||||
.f9b__pill:nth-child(5n+3) { border-bottom-color: #e9a804; }
|
||||
.f9b__pill:nth-child(5n+4) { border-bottom-color: #919f00; }
|
||||
.f9b__pill:nth-child(5n) { border-bottom-color: #0d6361; }
|
||||
|
||||
.f9b__pill-label {
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
color: #144838;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
/* arrow indicator — Figma frame 9 의 좌측 화살표 (▶) approximation */
|
||||
.f9b__pill-label::before {
|
||||
content: "▶";
|
||||
flex-shrink: 0;
|
||||
color: currentColor;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* 2026-05-14 — bullet 줄간격 1/3 으로 압축 : line-height 1.3 → 1.05 (약 1/3 감소).
|
||||
bullet 간 gap 2 → 1 로 더 압축. 가독성 임계 (한 bullet 안 line wrap 시 라인 겹침 직전). */
|
||||
.f9b__pill-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
color: #0c271e;
|
||||
font-size: var(--font-body);
|
||||
line-height: 1.05;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
/* 첫 line = 인용구 (italic + 무 마커) — 04-1 의 따옴표 인용구 패턴 */
|
||||
.f9b__pill-body .text-line:first-child {
|
||||
font-style: italic;
|
||||
color: #5C3714;
|
||||
padding-left: 0;
|
||||
}
|
||||
/* 이후 lines = 각 이슈 bullet (▪ 마커로 구분 강화) */
|
||||
.f9b__pill-body .text-line:not(:first-child) {
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.f9b__pill-body .text-line:not(:first-child)::before {
|
||||
content: "▪";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #919f00;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="f9b" data-frame-id="1171281180" data-template-id="pre_construction_model_info_stacked">
|
||||
<div class="f9b__title">1. DX에 대한 인식</div>
|
||||
<div class="f9b__body">
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill-rows">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill" data-frame-slot-id="pill_dynamic" data-pill-n="1">
|
||||
|
||||
<div class="f9b__pill-label">기술 및 소프트웨어 이해도</div>
|
||||
|
||||
|
||||
<div class="f9b__pill-body">
|
||||
<div class="text-line">"무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"</div><div class="text-line">기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.</div><div class="text-line">3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.</div><div class="text-line">필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill" data-frame-slot-id="pill_dynamic" data-pill-n="2">
|
||||
|
||||
<div class="f9b__pill-label">효과와 효율성</div>
|
||||
|
||||
|
||||
<div class="f9b__pill-body">
|
||||
<div class="text-line">"2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"</div><div class="text-line">성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.</div><div class="text-line">BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.</div><div class="text-line">결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill" data-frame-slot-id="pill_dynamic" data-pill-n="3">
|
||||
|
||||
<div class="f9b__pill-label">인력 및 교육</div>
|
||||
|
||||
|
||||
<div class="f9b__pill-body">
|
||||
<div class="text-line">"수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"</div><div class="text-line">교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.</div><div class="text-line">신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.</div><div class="text-line">여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill" data-frame-slot-id="pill_dynamic" data-pill-n="4">
|
||||
|
||||
<div class="f9b__pill-label">경제적 부담</div>
|
||||
|
||||
|
||||
<div class="f9b__pill-body">
|
||||
<div class="text-line">"S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"</div><div class="text-line">사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?</div><div class="text-line">모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.</div><div class="text-line">디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f9b__pill" data-frame-slot-id="pill_dynamic" data-pill-n="5">
|
||||
|
||||
<div class="f9b__pill-label">실무 및 적용성</div>
|
||||
|
||||
|
||||
<div class="f9b__pill-body">
|
||||
<div class="text-line">"실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"</div><div class="text-line">구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.</div><div class="text-line">지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.</div><div class="text-line">속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone" data-zone-position="bottom" data-template-id="process_product_two_way" style="grid-area: bottom;">
|
||||
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
|
||||
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
|
||||
|
||||
|
||||
<style>
|
||||
.f29b {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; flex-direction: column;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
word-break: keep-all;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f29b__title {
|
||||
font-size: var(--font-zone-title); /* 13px — frame 13 의 f13b__title 과 동일 위계 */
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-zone-title);
|
||||
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
flex-shrink: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* banners — top */
|
||||
.f29b__banners {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
}
|
||||
.f29b__banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
/* PROMOTED background gradients — figma origin lines 84-98 (header-bg--left/right).
|
||||
원본 SVG 3 개 → CSS gradient 로 재현 (R8/R9 준수). border-radius 9999px = 자동 pill. */
|
||||
.f29b__banner--left {
|
||||
background: linear-gradient(to right,
|
||||
rgba(136, 131, 118, 0.343) 0%,
|
||||
#39321E 73.1%,
|
||||
#39321E 100%);
|
||||
border-radius: 0 9999px 9999px 0;
|
||||
}
|
||||
.f29b__banner--right {
|
||||
background: linear-gradient(to right,
|
||||
#032118 0%,
|
||||
rgba(41, 107, 85, 0.1) 100%);
|
||||
border-radius: 9999px 0 0 9999px;
|
||||
}
|
||||
.f29b__banner-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 2px #322C1E;
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* PROMOTED gradient — lines 101-104 / 110-113 */
|
||||
.f29b__banner--left .f29b__banner-text {
|
||||
background-image:
|
||||
linear-gradient(90deg, rgb(62,53,35) 0%, rgb(62,53,35) 100%),
|
||||
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||
}
|
||||
.f29b__banner--right .f29b__banner-text {
|
||||
background-image:
|
||||
linear-gradient(90deg, rgb(34,95,74) 0%, rgb(34,95,74) 100%),
|
||||
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||
}
|
||||
|
||||
/* body — 6 cell grid (3 rows × 2 cols) — 좌우 row divider Y 정렬 */
|
||||
/* PROMOTED background — figma origin lines 53-68 (body-bg-left/right).
|
||||
좌(0~50%): #39321E α0.3 (top) → white α0.3 (bottom)
|
||||
우(50~100%): #296B55 α0.3 (top) → white α0.3 (bottom)
|
||||
원본 SVG → CSS multi-bg 로 재현. */
|
||||
.f29b__body {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
padding-top: 4px;
|
||||
background:
|
||||
linear-gradient(to bottom, rgba(57,50,30,0.3) 0%, rgba(255,255,255,0.3) 100%) 0% 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(좌, 우) */
|
||||
.f29b__grid {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-auto-rows: auto;
|
||||
gap: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
height: 100%;
|
||||
}
|
||||
.f29b__cell {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
/* row 2/3 (3 번째 cell 이후) 위에 dashed divider — 좌우 같은 Y */
|
||||
.f29b__cell:nth-child(n+3) {
|
||||
border-top: 1px dashed rgba(0,0,0,0.15);
|
||||
padding-top: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
/* section-title — 부소목차 (사용자 5-layer 직설 2026-05-07): Analogue 기반... 같은 깊이 0 굵은 bullet */
|
||||
.f29b__section-title {
|
||||
font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 5px;
|
||||
padding-left: 6px;
|
||||
border-left: 3px solid currentColor;
|
||||
}
|
||||
.f29b__cell--left .f29b__section-title { color: #5C3714; } /* PROMOTED line 125 */
|
||||
.f29b__cell--right .f29b__section-title { color: #084C56; } /* PROMOTED line 126 */
|
||||
.f29b__section-title__sub {
|
||||
font-size: var(--font-caption); /* 10px */
|
||||
margin-left: 3px;
|
||||
}
|
||||
/* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override. */
|
||||
.f29b__cell .text-line--bullet { color: #000; font-weight: 700; }
|
||||
/* 2026-05-14 — line-line gap 만 1/2 압축 (사용자 lock 정확 의도) :
|
||||
line-height 1.6 (공통 contract) 유지. .text-line + .text-line margin 4px → 2px. */
|
||||
.f29b__cell .text-line + .text-line { margin-top: 2px; }
|
||||
|
||||
/* 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 구조) */
|
||||
.f29b__transform {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 4px;
|
||||
align-items: start;
|
||||
}
|
||||
.f29b__transform__list {
|
||||
/* .text-line 공통 contract 그대로. wrapper 만 grid 안의 cell 역할 */
|
||||
}
|
||||
.f29b__transform__arrow {
|
||||
align-self: center;
|
||||
width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.f29b__transform__arrow img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.f29b__footnote {
|
||||
font-size: var(--font-caption); /* 10px */
|
||||
line-height: var(--lh-caption);
|
||||
color: #333;
|
||||
padding-left: 14px;
|
||||
margin-top: 1px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.f29b__aster-small { font-size: 9px; }
|
||||
</style>
|
||||
|
||||
<div class="f29b" data-frame-id="1171281210" data-template-id="process_product_two_way">
|
||||
|
||||
|
||||
<div class="f29b__title">2. DX 추진의 실태</div>
|
||||
|
||||
|
||||
<div class="f29b__banners">
|
||||
<div class="f29b__banner f29b__banner--left">
|
||||
<div class="f29b__banner-text">정책 및 발주 체계</div>
|
||||
</div>
|
||||
<div class="f29b__banner f29b__banner--right">
|
||||
<div class="f29b__banner-text">조직 및 수행 역량</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="f29b__body">
|
||||
|
||||
<div class="f29b__grid">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">실질적 기술 경쟁을 저해하는 정책 집행</div>
|
||||
|
||||
<div class="text-line text-line--bullet">모든 설계사가 수행 능력을 갖추었다는 전제하에 정책 시행</div><div class="text-line text-line--bullet">수행 능력이 없는 업체 선정 후 성과품의 수준을 낮추어 시행</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">공학적 개념 정립 부재</div>
|
||||
<div class="text-line text-line--bullet">DX와 BIM의 차이점을 명확히 구분하지 못하고 접근 방식과 기술적 도구 사이의 혼란만 가중</div><div class="text-line text-line--bullet">단순히 기술적 도구의 사용에 초점을 맞추느라 3D모델 제작 S/W에 과도하게 의존</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">적용 효과가 있는 사례도 없이 방침부터 도입</div>
|
||||
|
||||
<div class="text-line text-line--bullet">DX/BIM 적용에 따른 실무적 이득이 있다고 판단된 사례 부족</div><div class="text-line text-line--bullet">BIM 지침/방침 등을 시행 경험과 효과 검증도 없이 남발</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">‘본업 기술력 확보’ 우선의 개념 부재</div>
|
||||
<div class="text-line text-line--bullet">고도의 전문지식과 현장 경험이 축적된 Manual의 중요성과 필요성에 대한 이해 부족</div><div class="text-line text-line--bullet">국가·발주처의 지침·방침에만 의존한 업체의 기술력</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">엔지니어링 S/W에 대한 개념 부재</div>
|
||||
|
||||
<div class="text-line text-line--bullet">다양한 엔지니어링 S/W의 특성에 대한 깊은 이해 없이 범용 S/W 선택</div><div class="text-line text-line--bullet">대형 Global S/W 회사에 과도한 의존과 이에 예속되는 방침 남발로 전용 S/W 소멸</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">DX/BIM의 근본 취지와 목표의 이해 부족</div>
|
||||
<div class="text-line text-line--bullet">DX에 의한 과정의 혁신과 결과물의 변화에 대한 고민 부재</div><div class="text-line text-line--bullet">기술자가 직접 3D모델을 만들고 수정하며 설계를 수행하지 않고, 별도로 외주 처리하여 본질 회피</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||
|
||||
<div class="f29b__section-title">기술투자(R&D) 없는 성과 창출 기대</div>
|
||||
|
||||
<div class="text-line text-line--bullet">단순 BIM S/W만 구입하면 될 것이라는 안일한 생각</div><div class="text-line text-line--bullet">실질적 기술 개발 투자 노력 없이 남들이 하는 대로 하면 된다는 착각</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||
|
||||
<div class="f29b__section-title">과거의 타성에 머무르고 있는 기술자 집단</div>
|
||||
<div class="text-line text-line--bullet">설계/감리/시공 임직원들의 Digital 무지와 전략적 무지</div><div class="text-line text-line--bullet">교육과 학습을 통한 인재 양성보다 당장 실무 활용이 가능한 타사 인력 빼오기에 집중</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="f29b__popups" style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; padding: 0 8px;">
|
||||
<div>
|
||||
|
||||
<div class="f29b__popup-link">발주처 반응</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div class="f29b__popup-link">설계·시공업계 반응</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slide-footer">
|
||||
<div class="slide-footer-text">검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족은 DX 지연을 반복시키고 있다.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 299 KiB |
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: DX 지연 요인
|
||||
sidebar:
|
||||
order: 03
|
||||
---
|
||||
|
||||
## 1. DX에 대한 인식
|
||||
|
||||
- **기술 및 소프트웨어 이해도**
|
||||
- "무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다"
|
||||
- 기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.
|
||||
- 3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.
|
||||
- 필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?
|
||||
- **효과와 효율성**
|
||||
- "2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다"
|
||||
- 성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.
|
||||
- BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.
|
||||
- 결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?
|
||||
- **인력 및 교육**
|
||||
- "수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나"
|
||||
- 교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.
|
||||
- 신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.
|
||||
- 여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.
|
||||
- **경제적 부담**
|
||||
- "S/W 구독료만 크게 발생되고, 비용 보전은 안 된다"
|
||||
- 사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?
|
||||
- 모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.
|
||||
- 디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?
|
||||
- **실무 및 적용성**
|
||||
- "실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다"
|
||||
- 구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.
|
||||
- 지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.
|
||||
- 속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
## 2. DX 추진의 실태
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 정책 및 발주 체계
|
||||
- **실질적 기술 경쟁을 저해하는 정책 집행**
|
||||
- 모든 설계사가 수행 능력을 갖추었다는 전제하에 정책 시행
|
||||
- 수행 능력이 없는 업체 선정 후 성과품의 수준을 낮추어 시행
|
||||
<br/>
|
||||
- **적용 효과가 있는 사례도 없이 방침부터 도입**
|
||||
- DX/BIM 적용에 따른 실무적 이득이 있다고 판단된 사례 부족
|
||||
- BIM 지침/방침 등을 시행 경험과 효과 검증도 없이 남발
|
||||
<br/>
|
||||
- **엔지니어링 S/W에 대한 개념 부재**
|
||||
- 다양한 엔지니어링 S/W의 특성에 대한 깊은 이해 없이 범용 S/W 선택
|
||||
- 대형 Global S/W 회사에 과도한 의존과 이에 예속되는 방침 남발로 전용 S/W 소멸
|
||||
<br/>
|
||||
- **기술투자(R&D) 없는 성과 창출 기대**
|
||||
- 단순 BIM S/W만 구입하면 될 것이라는 안일한 생각
|
||||
- 실질적 기술 개발 투자 노력 없이 남들이 하는 대로 하면 된다는 착각
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>발주처 반응</summary>
|
||||
<img src="/assets/images/발주처반응.png" />
|
||||
</details>
|
||||
|
||||
### 2.2 조직 및 수행 역량
|
||||
- **공학적 개념 정립 부재**
|
||||
- DX와 BIM의 차이점을 명확히 구분하지 못하고 접근 방식과 기술적 도구 사이의 혼란만 가중
|
||||
- 단순히 기술적 도구의 사용에 초점을 맞추느라 3D모델 제작 S/W에 과도하게 의존
|
||||
<br/>
|
||||
- **‘본업 기술력 확보’ 우선의 개념 부재**
|
||||
- 고도의 전문지식과 현장 경험이 축적된 Manual의 중요성과 필요성에 대한 이해 부족
|
||||
- 국가·발주처의 지침·방침에만 의존한 업체의 기술력
|
||||
<br/>
|
||||
- **DX/BIM의 근본 취지와 목표의 이해 부족**
|
||||
- DX에 의한 과정의 혁신과 결과물의 변화에 대한 고민 부재
|
||||
- 기술자가 직접 3D모델을 만들고 수정하며 설계를 수행하지 않고, 별도로 외주 처리하여 본질 회피
|
||||
<br/>
|
||||
- **과거의 타성에 머무르고 있는 기술자 집단**
|
||||
- 설계/감리/시공 임직원들의 Digital 무지와 전략적 무지
|
||||
- 교육과 학습을 통한 인재 양성보다 당장 실무 활용이 가능한 타사 인력 빼오기에 집중
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>설계·시공업계 반응</summary>
|
||||
<img src="/assets/images/설계시공업계반응.png" />
|
||||
</details>
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족은 DX 지연을 반복시키고 있다.
|
||||
:::
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"step_num": 1,
|
||||
"step_name": "mdx_upload",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"samples\\mdx\\04. DX 지연 요인.mdx"
|
||||
],
|
||||
"output": [
|
||||
"step01_mdx_upload.json",
|
||||
"step01_mdx_source.md"
|
||||
],
|
||||
"note": "MDX 원본 그대로 step01_mdx_source.md 에 복사.",
|
||||
"data": {
|
||||
"mdx_path": "samples\\mdx\\04. DX 지연 요인.mdx",
|
||||
"run_id": "mdx04_b3_v12",
|
||||
"run_dir": "d:\\ad-hoc\\kei\\design_agent\\data\\runs\\mdx04_b3_v12\\phase_z2",
|
||||
"mdx_source_size_bytes": 4710,
|
||||
"mdx_source_lines": 89
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"step_num": 2,
|
||||
"step_name": "normalized",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step01_mdx_source.md"
|
||||
],
|
||||
"output": [
|
||||
"step02_normalized.json"
|
||||
],
|
||||
"note": "parse_mdx 결과: title / sections / footer 분리 + raw_content 보존. heading tree 미생성, orphan / details 감지 미완 (Step 2 ⚠ partial — 별 axis). orphans / details 필드는 schema lock — 빈 배열이라도 'detection 미수행' marker. stage0_adapter_diagnostics = IMP-02 chained adapter trace (default OFF canary). stage0_normalized_assets = IMP-03 Step 3 slide-level handoff (popups/images/tables list).",
|
||||
"data": {
|
||||
"slide_title": "DX 지연 요인",
|
||||
"slide_footer": "검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족은 DX 지연을 반복시키고 있다.",
|
||||
"sections_count": 2,
|
||||
"sections": [
|
||||
{
|
||||
"section_id": "04-1",
|
||||
"section_num": 1,
|
||||
"title": "1. DX에 대한 인식",
|
||||
"raw_content_length": 934,
|
||||
"raw_content": "- **기술 및 소프트웨어 이해도**\n - \"무슨 말인지 잘 모르겠다, 어디까지 어떻게 해야 하는지 모르겠다\"\n - 기본지침, 시행지침 등 새롭게 알아야 할 게 너무 많다.\n - 3D 모델과 별 차이점을 모르겠다. S/W마다 사용법이 다르다.\n - 필요한 것은 쉽고 간단한 건데, 왜 이렇게 복잡하게 만들까?\n- **효과와 효율성**\n - \"2D 설계 후 전환설계를 수행하는데 효과는 모르겠고, 효율은 낮다\"\n - 성과품 작성은 기존과 같게 하고, 추가 업무만 발생해 효율이 낮다.\n - BIM으로 인해 가중되는 업무 대비 효과가 거의 없어 보인다.\n - 결과적으로 큰 차이를 못 느끼겠고, 이런 노력이 정말 가치 있는 일이야?\n- **인력 및 교육**\n - \"수행 인력이 부족하고, 기존 직원들은 어떻게 교육해야 하나\"\n - 교육시간 손실로 일손이 더 필요해지고, 적응하는데도 시간이 걸린다.\n - 신입사원은 없고 BIM 수행을 할 수 있는 기술자가 부족하다.\n - 여러 회사의 S/W별 사용법이 달라 새로운 S/W에 적용에 시간이 필요하다.\n- **경제적 부담**\n - \"S/W 구독료만 크게 발생되고, 비용 보전은 안 된다\"\n - 사용해야 할 S/W의 종류가 너무 많고 복잡한데 모두 필요한가?\n - 모든 Project에 적용되는 것도 아닌데, 다수의 S/W 구독료를 내야 한다.\n - 디지털전환 하기 위해 이 비용을 쓰는게 정말 경제적인 이득이 있는 거야?\n- **실무 및 적용성**\n - \"실무적 사용에 의한 효율성 증진보다는 홍보, PQ용으로 사용한다\"\n - 구체적 적용에 의한 비용, 시간, 품질 등의 효과 사례가 없다.\n - 지형, 선형, 도로, 교량 Model을 만드는 S/W가 모두 달라 적용이 어렵다.\n - 속성정보를 반영하지 않은 형상 위주의 3D 모델 제작에만 초점이 맞춰져 있다.\n<br/>\n---\n<br/>"
|
||||
},
|
||||
{
|
||||
"section_id": "04-2",
|
||||
"section_num": 2,
|
||||
"title": "2. DX 추진의 실태",
|
||||
"raw_content_length": 1349,
|
||||
"raw_content": "<br/>\n\n### 2.1 정책 및 발주 체계\n- **실질적 기술 경쟁을 저해하는 정책 집행** \n - 모든 설계사가 수행 능력을 갖추었다는 전제하에 정책 시행\n - 수행 능력이 없는 업체 선정 후 성과품의 수준을 낮추어 시행 \n<br/>\n- **적용 효과가 있는 사례도 없이 방침부터 도입** \n - DX/BIM 적용에 따른 실무적 이득이 있다고 판단된 사례 부족\n - BIM 지침/방침 등을 시행 경험과 효과 검증도 없이 남발\n<br/>\n- **엔지니어링 S/W에 대한 개념 부재** \n - 다양한 엔지니어링 S/W의 특성에 대한 깊은 이해 없이 범용 S/W 선택\n - 대형 Global S/W 회사에 과도한 의존과 이에 예속되는 방침 남발로 전용 S/W 소멸\n<br/>\n- **기술투자(R&D) 없는 성과 창출 기대** \n - 단순 BIM S/W만 구입하면 될 것이라는 안일한 생각\n - 실질적 기술 개발 투자 노력 없이 남들이 하는 대로 하면 된다는 착각\n<br/>\n<details>\n <summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>발주처 반응</summary>\n <img src=\"/assets/images/발주처반응.png\" />\n</details>\n\n### 2.2 조직 및 수행 역량\n- **공학적 개념 정립 부재** \n - DX와 BIM의 차이점을 명확히 구분하지 못하고 접근 방식과 기술적 도구 사이의 혼란만 가중\n - 단순히 기술적 도구의 사용에 초점을 맞추느라 3D모델 제작 S/W에 과도하게 의존\n<br/>\n- **‘본업 기술력 확보’ 우선의 개념 부재** \n - 고도의 전문지식과 현장 경험이 축적된 Manual의 중요성과 필요성에 대한 이해 부족\n - 국가·발주처의 지침·방침에만 의존한 업체의 기술력 \n<br/>\n- **DX/BIM의 근본 취지와 목표의 이해 부족** \n - DX에 의한 과정의 혁신과 결과물의 변화에 대한 고민 부재\n - 기술자가 직접 3D모델을 만들고 수정하며 설계를 수행하지 않고, 별도로 외주 처리하여 본질 회피\n<br/>\n- **과거의 타성에 머무르고 있는 기술자 집단** \n - 설계/감리/시공 임직원들의 Digital 무지와 전략적 무지\n - 교육과 학습을 통한 인재 양성보다 당장 실무 활용이 가능한 타사 인력 빼오기에 집중\n<br/>\n<details>\n <summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>설계·시공업계 반응</summary>\n <img src=\"/assets/images/설계시공업계반응.png\" />\n</details>\n<br/>\n---"
|
||||
}
|
||||
],
|
||||
"orphans": [],
|
||||
"details": [],
|
||||
"stage0_adapter_diagnostics": {
|
||||
"enabled": false,
|
||||
"used": false,
|
||||
"fallback_reason": null,
|
||||
"id_reconstruction_log": [],
|
||||
"adapter_counts": null,
|
||||
"legacy_counts": {
|
||||
"sections": 2
|
||||
}
|
||||
},
|
||||
"stage0_normalized_assets": {
|
||||
"popups": [],
|
||||
"images": [],
|
||||
"tables": []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"step_num": 5,
|
||||
"step_name": "v4_evidence",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step02_normalized.json",
|
||||
"tests/matching/v4_full32_result.yaml"
|
||||
],
|
||||
"output": [
|
||||
"step05_v4_evidence.json"
|
||||
],
|
||||
"note": "V4 non-reject max-6 후보 list (Step 9 application_plan input). raw 32 entry 는 tests/matching/v4_full32_result.yaml 에 영속. candidate_status='ok' = 후보 1개 이상 / 'no_non_reject_v4_candidate' = 0개 (Step 9 fallback path 입력). Step 6 plan_composition() 은 lookup_v4_match() (rank-1) 그대로 사용 — backward compat (Step 6-A axis 까지).",
|
||||
"data": {
|
||||
"v4_source": "tests\\matching\\v4_full32_result.yaml",
|
||||
"aligned_section_ids": [
|
||||
"04-1",
|
||||
"04-2"
|
||||
],
|
||||
"evidence_per_section": [
|
||||
{
|
||||
"section_id": "04-1",
|
||||
"v4_candidates": [],
|
||||
"candidate_status": "no_non_reject_v4_candidate"
|
||||
},
|
||||
{
|
||||
"section_id": "04-2",
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit"
|
||||
}
|
||||
],
|
||||
"candidate_status": "ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"step_num": 6,
|
||||
"step_name": "composition_plan",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step02_normalized.json",
|
||||
"step05_v4_evidence.json"
|
||||
],
|
||||
"output": [
|
||||
"step06_composition_plan.json"
|
||||
],
|
||||
"note": "composition v0 count-based — sections → candidates → score → greedy select. Step 6-A (사용자 lock 2026-05-08): selected_units[i].v4_candidates 추가 (non-reject max-6 후보 list, candidates[0] = 단일 frame_* 와 일관). logic 무변 — runtime 결과 동일. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"selected_units_count": 2,
|
||||
"layout_preset_decided": "horizontal-2",
|
||||
"candidates_summary": [
|
||||
{
|
||||
"source_section_ids": [
|
||||
"04-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"label": "reject",
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"v4_rank": 1,
|
||||
"selection_path": "rank_1",
|
||||
"fallback_reason": null,
|
||||
"score": 0.0,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 5,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": null,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"source_section_ids": [
|
||||
"04-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"template_id": "process_product_two_way",
|
||||
"label": "light_edit",
|
||||
"phase_z_status": "adapt_matched_zone",
|
||||
"v4_rank": 2,
|
||||
"selection_path": "rank_2_fallback",
|
||||
"fallback_reason": "capacity_mismatch:strict_mismatch",
|
||||
"score": 0.53977,
|
||||
"selection_state": "selected",
|
||||
"auto_selectable": true,
|
||||
"filter_reasons": [],
|
||||
"notes": [],
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"candidates_total": 2,
|
||||
"candidates_viable_auto": 2,
|
||||
"selected_units": [
|
||||
{
|
||||
"source_section_ids": [
|
||||
"04-1"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"frame_id": "1171281180",
|
||||
"frame_number": 9,
|
||||
"frame_template_id": "pre_construction_model_info_stacked",
|
||||
"label": "reject",
|
||||
"v4_rank": 1,
|
||||
"selection_path": "rank_1",
|
||||
"fallback_reason": null,
|
||||
"score": 0.0,
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 5,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": null,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.0,
|
||||
"confidence": 0.7223,
|
||||
"label": "reject",
|
||||
"label_weight": 0.0,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"notes": [],
|
||||
"v4_candidates": []
|
||||
},
|
||||
{
|
||||
"source_section_ids": [
|
||||
"04-2"
|
||||
],
|
||||
"merge_type": "single",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"frame_template_id": "process_product_two_way",
|
||||
"label": "light_edit",
|
||||
"v4_rank": 2,
|
||||
"selection_path": "rank_2_fallback",
|
||||
"fallback_reason": "capacity_mismatch:strict_mismatch",
|
||||
"score": 0.53977,
|
||||
"phase_z_status": "adapt_matched_zone",
|
||||
"rationale": {
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
},
|
||||
"frame_compat": 0.5398,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit",
|
||||
"label_weight": 0.7,
|
||||
"merge_type": "single",
|
||||
"hierarchy_coherence": null,
|
||||
"density_score": null
|
||||
},
|
||||
"notes": [],
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"step_num": 7,
|
||||
"step_name": "layout",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step06_composition_plan.json"
|
||||
],
|
||||
"output": [
|
||||
"step07_layout.json",
|
||||
"step07_selected_layout.html"
|
||||
],
|
||||
"note": "count-based v0 — 들여쓰기 / 정렬 미세 layout 미구현 (Step 7 ⚠ partial). Step 7-conn (사용자 lock 2026-05-08): unit_count → layout_candidates list (select_layout_candidates) artifact 기록. default 결정 자체는 무변 — candidates[0] 가 default (default_selection: true), 나머지는 alternative. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_css": {
|
||||
"areas": "\"top\" \"bottom\"",
|
||||
"cols": "1fr",
|
||||
"rows": "211px 360px",
|
||||
"heights_px": [
|
||||
211,
|
||||
360
|
||||
],
|
||||
"ratios": [
|
||||
0.361,
|
||||
0.615
|
||||
],
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"dynamic_rows": true,
|
||||
"raw_zone_layout": {
|
||||
"computation": "min_height_first + content_weight_distribution",
|
||||
"slide_body_height": 585,
|
||||
"gap": 14,
|
||||
"available_after_gap": 571,
|
||||
"min_heights_px": [
|
||||
200,
|
||||
345
|
||||
],
|
||||
"min_scaled": false,
|
||||
"total_min_height": 545,
|
||||
"remaining_after_min": 26,
|
||||
"content_weights": [
|
||||
{
|
||||
"position": "top",
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"score": 6.168
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"template_id": "process_product_two_way",
|
||||
"score": 8.486
|
||||
}
|
||||
],
|
||||
"weight_shares": [
|
||||
0.421,
|
||||
0.579
|
||||
],
|
||||
"extras_px": [
|
||||
11,
|
||||
15
|
||||
],
|
||||
"heights_px": [
|
||||
211,
|
||||
360
|
||||
],
|
||||
"ratios": [
|
||||
0.361,
|
||||
0.615
|
||||
]
|
||||
}
|
||||
},
|
||||
"zones_count": 2,
|
||||
"unit_count": 2,
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
],
|
||||
"layout_override_applied": false,
|
||||
"auto_layout_preset": "horizontal-2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"step_num": 8,
|
||||
"step_name": "zone_region_ratios",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step07_layout.json",
|
||||
"step10_frame_contract.json"
|
||||
],
|
||||
"output": [
|
||||
"step08_zone_region_ratios.json",
|
||||
"step08_zone_content_placement.html"
|
||||
],
|
||||
"note": "계획값 only. 실측값은 Step 14 (visual_check). zone-level 만 dynamic, region-level (sub_zone 안 sections) 은 균등 분배 (1/1/1). F29 처럼 비균등 콘텐츠 (transform_table + plain) 는 첫 cell 부족 가능 — Step 8 ⚠ partial. Step 8-conn (사용자 lock 2026-05-08): per_zone_plan[i] 에 region_layout_candidates + display_strategy_candidates 추가 (passive 기록). placeholder signals = text-only / region_count=1 (Step 3/4 부재 종속). 실제 content_object 신호 활성화는 별 axis. Step 9 application_plan input.",
|
||||
"data": {
|
||||
"zone_heights_px_planned": [
|
||||
211,
|
||||
360
|
||||
],
|
||||
"zone_ratios_planned": [
|
||||
0.361,
|
||||
0.615
|
||||
],
|
||||
"per_zone_plan": [
|
||||
{
|
||||
"position": "top",
|
||||
"zone_height_px_planned": 211,
|
||||
"zone_ratio_planned": 0.361,
|
||||
"min_height_px": 200,
|
||||
"frame_cardinality_strict": null,
|
||||
"sub_zones_planned": [
|
||||
{
|
||||
"id": "pill_dynamic",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": null,
|
||||
"partial_target_path": ".f9b__pill-rows .f9b__pill:nth-child(N)"
|
||||
}
|
||||
],
|
||||
"child_distribution_note": "현재 child zone (sub_zones 안 sections) 균등 분배 — Step 8 region-level ratio ⚠ partial.",
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
]
|
||||
},
|
||||
{
|
||||
"position": "bottom",
|
||||
"zone_height_px_planned": 360,
|
||||
"zone_ratio_planned": 0.615,
|
||||
"min_height_px": 345,
|
||||
"frame_cardinality_strict": 2,
|
||||
"sub_zones_planned": [
|
||||
{
|
||||
"id": "process_column",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block",
|
||||
"transform_table"
|
||||
],
|
||||
"cardinality_strict": null,
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--left"
|
||||
},
|
||||
{
|
||||
"id": "product_column",
|
||||
"role": "main_text",
|
||||
"accepts": [
|
||||
"text_block"
|
||||
],
|
||||
"cardinality_strict": null,
|
||||
"partial_target_path": ".f29b__grid .f29b__cell--right"
|
||||
}
|
||||
],
|
||||
"child_distribution_note": "현재 child zone (sub_zones 안 sections) 균등 분배 — Step 8 region-level ratio ⚠ partial.",
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
]
|
||||
}
|
||||
],
|
||||
"step8_conn_placeholder_signals": {
|
||||
"region_count": 1,
|
||||
"content_type": "text_block",
|
||||
"flow_type": null,
|
||||
"role_pattern": null,
|
||||
"ratio_asymmetric": false,
|
||||
"has_visual_element": false,
|
||||
"details_presence": false,
|
||||
"large_table": false,
|
||||
"long_text": false,
|
||||
"fits_in_region": null,
|
||||
"_note": "Step 3/4 부재 종속 placeholder — 모든 zone 을 text-only / region_count=1 로 가정. 실제 content_object 신호 활성화는 별 axis."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,893 @@
|
||||
{
|
||||
"step_num": 9,
|
||||
"step_name": "application_plan",
|
||||
"step_status": "partial",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step05_v4_evidence.json",
|
||||
"step06_composition_plan.json",
|
||||
"step07_layout.json",
|
||||
"step08_zone_region_ratios.json"
|
||||
],
|
||||
"output": [
|
||||
"step09_application_plan.json",
|
||||
"step09_application_plan.html"
|
||||
],
|
||||
"note": "Step 9 v0 passive application_plan trace (사용자 lock 2026-05-08). V4 후보 + layout / region / display 후보 + V4 label → application_mode 변환을 side-by-side 로 기록. v0 invariant 5 가지 (status.md §4) 만족. Step 6 의 default 결정 그대로 (current_default_candidate). auto decision 은 Step 9 v1 (별 axis). region/display 후보는 Step 8-conn 의 placeholder signal 종속 (Step 3/4 부재). Step 7-A axis (2026-05-08): frame_overrides_applied 가 사용자 LayoutPanel/FramePanel 선택값을 강제 적용한 trace.",
|
||||
"data": {
|
||||
"units": [
|
||||
{
|
||||
"unit_id": "04-1",
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
],
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
],
|
||||
"candidate_status": "no_non_reject_v4_candidate",
|
||||
"application_status": "no_v4_candidate",
|
||||
"current_default_candidate": null,
|
||||
"selected_v4_rank": 1,
|
||||
"selection_path": "rank_1",
|
||||
"fallback_used": false,
|
||||
"fallback_reason": null,
|
||||
"candidate_evidence": [
|
||||
{
|
||||
"rank": 1,
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"frame_id": "1171281180",
|
||||
"frame_number": 9,
|
||||
"confidence": 0.7223,
|
||||
"label": "reject",
|
||||
"v4_label": "reject",
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "design_reference_only",
|
||||
"decision": "selected",
|
||||
"reason": "primary_selected",
|
||||
"capacity_fit": {
|
||||
"item_count": 5,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": null,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"fallback_chain": [
|
||||
{
|
||||
"rank": 1,
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"frame_id": "1171281180",
|
||||
"frame_number": 9,
|
||||
"confidence": 0.7223,
|
||||
"label": "reject",
|
||||
"v4_label": "reject",
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "design_reference_only",
|
||||
"decision": "selected",
|
||||
"reason": "primary_selected",
|
||||
"capacity_fit": {
|
||||
"item_count": 5,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": null,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"v4_candidates": [],
|
||||
"v4_all_judgments": [
|
||||
{
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"frame_id": "1171281180",
|
||||
"frame_number": 9,
|
||||
"v4_rank": 1,
|
||||
"confidence": 0.7223,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "field_effectiveness_five_elements",
|
||||
"frame_id": "1171281181",
|
||||
"frame_number": 10,
|
||||
"v4_rank": 2,
|
||||
"confidence": 0.6586,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "policy_achievement_five_goals",
|
||||
"frame_id": "1171281213",
|
||||
"frame_number": 32,
|
||||
"v4_rank": 3,
|
||||
"confidence": 0.6514,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "compensation_complaint_side_card",
|
||||
"frame_id": "1171281176",
|
||||
"frame_number": 5,
|
||||
"v4_rank": 4,
|
||||
"confidence": 0.6058,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "solution_engn_split_diagram",
|
||||
"frame_id": "1171281201",
|
||||
"frame_number": 21,
|
||||
"v4_rank": 5,
|
||||
"confidence": 0.4773,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "model_specialized_engn_sw",
|
||||
"frame_id": "1171281202",
|
||||
"frame_number": 22,
|
||||
"v4_rank": 6,
|
||||
"confidence": 0.4624,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "commercial_sw_four_categories",
|
||||
"frame_id": "1171281205",
|
||||
"frame_number": 25,
|
||||
"v4_rank": 7,
|
||||
"confidence": 0.4293,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "construction_bim_three_usage",
|
||||
"frame_id": "1171281182",
|
||||
"frame_number": 11,
|
||||
"v4_rank": 8,
|
||||
"confidence": 0.4234,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "sw_dependency_four_problems",
|
||||
"frame_id": "1171281206",
|
||||
"frame_number": 26,
|
||||
"v4_rank": 9,
|
||||
"confidence": 0.3986,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "engn_sw_development_domain_knowledge",
|
||||
"frame_id": "1171281173",
|
||||
"frame_number": 2,
|
||||
"v4_rank": 10,
|
||||
"confidence": 0.3942,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "sw_development_cycle_six_nodes",
|
||||
"frame_id": "1171281172",
|
||||
"frame_number": 1,
|
||||
"v4_rank": 11,
|
||||
"confidence": 0.3899,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "policy_goals_plus_execution_requirements",
|
||||
"frame_id": "1171281192",
|
||||
"frame_number": 15,
|
||||
"v4_rank": 12,
|
||||
"confidence": 0.3833,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "bim_current_problems_paired",
|
||||
"frame_id": "1171281194",
|
||||
"frame_number": 17,
|
||||
"v4_rank": 13,
|
||||
"confidence": 0.3773,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "overseas_bim_numbered_list",
|
||||
"frame_id": "1171281174",
|
||||
"frame_number": 3,
|
||||
"v4_rank": 14,
|
||||
"confidence": 0.3748,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"v4_rank": 15,
|
||||
"confidence": 0.3713,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "bim_issues_quadrant_four",
|
||||
"frame_id": "1171281193",
|
||||
"frame_number": 16,
|
||||
"v4_rank": 16,
|
||||
"confidence": 0.3699,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"frame_number": 20,
|
||||
"v4_rank": 17,
|
||||
"confidence": 0.3158,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "domestic_bim_actor_relations",
|
||||
"frame_id": "1171281175",
|
||||
"frame_number": 4,
|
||||
"v4_rank": 18,
|
||||
"confidence": 0.2897,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "bigroom_system_components",
|
||||
"frame_id": "1171281178",
|
||||
"frame_number": 7,
|
||||
"v4_rank": 19,
|
||||
"confidence": 0.2742,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "info_management_what_how_when",
|
||||
"frame_id": "1171281179",
|
||||
"frame_number": 8,
|
||||
"v4_rank": 20,
|
||||
"confidence": 0.2604,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"v4_rank": 21,
|
||||
"confidence": 0.2313,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "compensation_complaint_map",
|
||||
"frame_id": "1171281177",
|
||||
"frame_number": 6,
|
||||
"v4_rank": 22,
|
||||
"confidence": 0.201,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "sw_reality_three_emphasis",
|
||||
"frame_id": "1171281209",
|
||||
"frame_number": 28,
|
||||
"v4_rank": 23,
|
||||
"confidence": 0.201,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "bim_dx_comparison_table",
|
||||
"frame_id": "1171281195",
|
||||
"frame_number": 18,
|
||||
"v4_rank": 24,
|
||||
"confidence": 0.1798,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "three_persona_benefits",
|
||||
"frame_id": "1171281191",
|
||||
"frame_number": 14,
|
||||
"v4_rank": 25,
|
||||
"confidence": 0.1787,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "construction_goals_three_circle_intersection",
|
||||
"frame_id": "1171281189",
|
||||
"frame_number": 12,
|
||||
"v4_rank": 26,
|
||||
"confidence": 0.1706,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"frame_number": 27,
|
||||
"v4_rank": 27,
|
||||
"confidence": 0.1399,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "engn_sw_three_types",
|
||||
"frame_id": "1171281204",
|
||||
"frame_number": 24,
|
||||
"v4_rank": 28,
|
||||
"confidence": 0.0929,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "design_method_distortion_three_col",
|
||||
"frame_id": "1171281197",
|
||||
"frame_number": 19,
|
||||
"v4_rank": 29,
|
||||
"confidence": 0.0584,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "app_sw_package_vs_solution",
|
||||
"frame_id": "1171281203",
|
||||
"frame_number": 23,
|
||||
"v4_rank": 30,
|
||||
"confidence": 0.0052,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "industry_current_status_three_col",
|
||||
"frame_id": "1171281211",
|
||||
"frame_number": 30,
|
||||
"v4_rank": 31,
|
||||
"confidence": 0.0044,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "industry_characteristics_three_col",
|
||||
"frame_id": "1171281212",
|
||||
"frame_number": 31,
|
||||
"v4_rank": 32,
|
||||
"confidence": 0.0029,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
}
|
||||
],
|
||||
"application_candidates": [],
|
||||
"position": null,
|
||||
"assignment_source": null,
|
||||
"section_assignment_override": false,
|
||||
"replaced_auto_unit": null,
|
||||
"skipped_collided_auto_units": [],
|
||||
"skipped_reason": null
|
||||
},
|
||||
{
|
||||
"unit_id": "04-2",
|
||||
"layout_preset": "horizontal-2",
|
||||
"layout_candidates": [
|
||||
"horizontal-2",
|
||||
"vertical-2"
|
||||
],
|
||||
"region_layout_candidates": [
|
||||
"region-single"
|
||||
],
|
||||
"display_strategy_candidates": [
|
||||
"inline_full",
|
||||
"inline_preview_with_details",
|
||||
"details_only"
|
||||
],
|
||||
"candidate_status": "ok",
|
||||
"application_status": "ok",
|
||||
"current_default_candidate": "process_product_two_way",
|
||||
"selected_v4_rank": 2,
|
||||
"selection_path": "rank_2_fallback",
|
||||
"fallback_used": true,
|
||||
"fallback_reason": "capacity_mismatch:strict_mismatch",
|
||||
"candidate_evidence": [
|
||||
{
|
||||
"rank": 1,
|
||||
"template_id": "bim_dx_comparison_table",
|
||||
"frame_id": "1171281195",
|
||||
"frame_number": 18,
|
||||
"confidence": 0.8152,
|
||||
"label": "reject",
|
||||
"v4_label": "reject",
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "design_reference_only",
|
||||
"decision": "skipped",
|
||||
"reason": "capacity_mismatch:strict_mismatch",
|
||||
"capacity_fit": {
|
||||
"item_count": 8,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "strict_mismatch",
|
||||
"mismatch_reason": "strict cardinality 2, content has 8 items. mapper 가 FitError 를 raise 할 것."
|
||||
}
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit",
|
||||
"v4_label": "light_edit",
|
||||
"phase_z_status": "adapt_matched_zone",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "deterministic_minor_adjustment",
|
||||
"decision": "selected",
|
||||
"reason": "fallback_selected",
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"fallback_chain": [
|
||||
{
|
||||
"rank": 1,
|
||||
"template_id": "bim_dx_comparison_table",
|
||||
"frame_id": "1171281195",
|
||||
"frame_number": 18,
|
||||
"confidence": 0.8152,
|
||||
"label": "reject",
|
||||
"v4_label": "reject",
|
||||
"phase_z_status": "fallback_candidate",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "design_reference_only",
|
||||
"decision": "skipped",
|
||||
"reason": "capacity_mismatch:strict_mismatch",
|
||||
"capacity_fit": {
|
||||
"item_count": 8,
|
||||
"source_shape": "top_bullets",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "strict_mismatch",
|
||||
"mismatch_reason": "strict cardinality 2, content has 8 items. mapper 가 FitError 를 raise 할 것."
|
||||
}
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit",
|
||||
"v4_label": "light_edit",
|
||||
"phase_z_status": "adapt_matched_zone",
|
||||
"catalog_registered": true,
|
||||
"filtered_for_direct_execution": false,
|
||||
"route_hint": "deterministic_minor_adjustment",
|
||||
"decision": "selected",
|
||||
"reason": "fallback_selected",
|
||||
"capacity_fit": {
|
||||
"item_count": 2,
|
||||
"source_shape": "h3_subsections",
|
||||
"capacity": {
|
||||
"strict": 2,
|
||||
"min": null,
|
||||
"max": null,
|
||||
"truncate_at": null,
|
||||
"pad_to": null
|
||||
},
|
||||
"fit_status": "ok",
|
||||
"mismatch_reason": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"v4_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"v4_rank": 2,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit"
|
||||
}
|
||||
],
|
||||
"v4_all_judgments": [
|
||||
{
|
||||
"template_id": "bim_dx_comparison_table",
|
||||
"frame_id": "1171281195",
|
||||
"frame_number": 18,
|
||||
"v4_rank": 1,
|
||||
"confidence": 0.8152,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"frame_number": 29,
|
||||
"v4_rank": 2,
|
||||
"confidence": 0.7711,
|
||||
"label": "light_edit",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "app_sw_package_vs_solution",
|
||||
"frame_id": "1171281203",
|
||||
"frame_number": 23,
|
||||
"v4_rank": 3,
|
||||
"confidence": 0.6389,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "engn_sw_three_types",
|
||||
"frame_id": "1171281204",
|
||||
"frame_number": 24,
|
||||
"v4_rank": 4,
|
||||
"confidence": 0.6372,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "industry_characteristics_three_col",
|
||||
"frame_id": "1171281212",
|
||||
"frame_number": 31,
|
||||
"v4_rank": 5,
|
||||
"confidence": 0.5996,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "bim_adoption_central_split",
|
||||
"frame_id": "1171281208",
|
||||
"frame_number": 27,
|
||||
"v4_rank": 6,
|
||||
"confidence": 0.55,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "bigroom_system_components",
|
||||
"frame_id": "1171281178",
|
||||
"frame_number": 7,
|
||||
"v4_rank": 7,
|
||||
"confidence": 0.5172,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "industry_current_status_three_col",
|
||||
"frame_id": "1171281211",
|
||||
"frame_number": 30,
|
||||
"v4_rank": 8,
|
||||
"confidence": 0.4774,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "design_method_distortion_three_col",
|
||||
"frame_id": "1171281197",
|
||||
"frame_number": 19,
|
||||
"v4_rank": 9,
|
||||
"confidence": 0.4179,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "sw_dependency_four_problems",
|
||||
"frame_id": "1171281206",
|
||||
"frame_number": 26,
|
||||
"v4_rank": 10,
|
||||
"confidence": 0.4138,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "bim_current_problems_paired",
|
||||
"frame_id": "1171281194",
|
||||
"frame_number": 17,
|
||||
"v4_rank": 11,
|
||||
"confidence": 0.3559,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "domestic_bim_actor_relations",
|
||||
"frame_id": "1171281175",
|
||||
"frame_number": 4,
|
||||
"v4_rank": 12,
|
||||
"confidence": 0.3457,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "pre_construction_model_info_stacked",
|
||||
"frame_id": "1171281180",
|
||||
"frame_number": 9,
|
||||
"v4_rank": 13,
|
||||
"confidence": 0.332,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "model_specialized_engn_sw",
|
||||
"frame_id": "1171281202",
|
||||
"frame_number": 22,
|
||||
"v4_rank": 14,
|
||||
"confidence": 0.3275,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "commercial_sw_four_categories",
|
||||
"frame_id": "1171281205",
|
||||
"frame_number": 25,
|
||||
"v4_rank": 15,
|
||||
"confidence": 0.3074,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "policy_goals_plus_execution_requirements",
|
||||
"frame_id": "1171281192",
|
||||
"frame_number": 15,
|
||||
"v4_rank": 16,
|
||||
"confidence": 0.2847,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "construction_goals_three_circle_intersection",
|
||||
"frame_id": "1171281189",
|
||||
"frame_number": 12,
|
||||
"v4_rank": 17,
|
||||
"confidence": 0.2558,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "bim_issues_quadrant_four",
|
||||
"frame_id": "1171281193",
|
||||
"frame_number": 16,
|
||||
"v4_rank": 18,
|
||||
"confidence": 0.2302,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "compensation_complaint_map",
|
||||
"frame_id": "1171281177",
|
||||
"frame_number": 6,
|
||||
"v4_rank": 19,
|
||||
"confidence": 0.2077,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "dx_sw_necessity_three_perspectives",
|
||||
"frame_id": "1171281198",
|
||||
"frame_number": 20,
|
||||
"v4_rank": 20,
|
||||
"confidence": 0.1842,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "three_parallel_requirements",
|
||||
"frame_id": "1171281190",
|
||||
"frame_number": 13,
|
||||
"v4_rank": 21,
|
||||
"confidence": 0.1399,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "sw_reality_three_emphasis",
|
||||
"frame_id": "1171281209",
|
||||
"frame_number": 28,
|
||||
"v4_rank": 22,
|
||||
"confidence": 0.1271,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "solution_engn_split_diagram",
|
||||
"frame_id": "1171281201",
|
||||
"frame_number": 21,
|
||||
"v4_rank": 23,
|
||||
"confidence": 0.1171,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "overseas_bim_numbered_list",
|
||||
"frame_id": "1171281174",
|
||||
"frame_number": 3,
|
||||
"v4_rank": 24,
|
||||
"confidence": 0.1007,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "policy_achievement_five_goals",
|
||||
"frame_id": "1171281213",
|
||||
"frame_number": 32,
|
||||
"v4_rank": 25,
|
||||
"confidence": 0.0751,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "three_persona_benefits",
|
||||
"frame_id": "1171281191",
|
||||
"frame_number": 14,
|
||||
"v4_rank": 26,
|
||||
"confidence": 0.0722,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "engn_sw_development_domain_knowledge",
|
||||
"frame_id": "1171281173",
|
||||
"frame_number": 2,
|
||||
"v4_rank": 27,
|
||||
"confidence": 0.0635,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "sw_development_cycle_six_nodes",
|
||||
"frame_id": "1171281172",
|
||||
"frame_number": 1,
|
||||
"v4_rank": 28,
|
||||
"confidence": 0.0633,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "field_effectiveness_five_elements",
|
||||
"frame_id": "1171281181",
|
||||
"frame_number": 10,
|
||||
"v4_rank": 29,
|
||||
"confidence": 0.0618,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "info_management_what_how_when",
|
||||
"frame_id": "1171281179",
|
||||
"frame_number": 8,
|
||||
"v4_rank": 30,
|
||||
"confidence": 0.0487,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
},
|
||||
{
|
||||
"template_id": "compensation_complaint_side_card",
|
||||
"frame_id": "1171281176",
|
||||
"frame_number": 5,
|
||||
"v4_rank": 31,
|
||||
"confidence": 0.0457,
|
||||
"label": "reject",
|
||||
"catalog_registered": false
|
||||
},
|
||||
{
|
||||
"template_id": "construction_bim_three_usage",
|
||||
"frame_id": "1171281182",
|
||||
"frame_number": 11,
|
||||
"v4_rank": 32,
|
||||
"confidence": 0.0,
|
||||
"label": "reject",
|
||||
"catalog_registered": true
|
||||
}
|
||||
],
|
||||
"application_candidates": [
|
||||
{
|
||||
"template_id": "process_product_two_way",
|
||||
"frame_id": "1171281210",
|
||||
"v4_label": "light_edit",
|
||||
"application_mode": "same_frame_with_adjustment",
|
||||
"auto_applicable": true,
|
||||
"required_changes": [],
|
||||
"delegated_to": "step10_contract_check"
|
||||
}
|
||||
],
|
||||
"position": null,
|
||||
"assignment_source": null,
|
||||
"section_assignment_override": false,
|
||||
"replaced_auto_unit": null,
|
||||
"skipped_collided_auto_units": [],
|
||||
"skipped_reason": null
|
||||
}
|
||||
],
|
||||
"candidate_status_summary": {
|
||||
"units_with_no_v4_candidate": [
|
||||
"04-1"
|
||||
],
|
||||
"units_with_fallback": [
|
||||
"04-2"
|
||||
]
|
||||
},
|
||||
"fallback_policy": {
|
||||
"fallback_used_count": 1,
|
||||
"fallback_selection_count": 1,
|
||||
"chain_exhausted_count": 0,
|
||||
"selection_paths": [
|
||||
{
|
||||
"section_id": "04-1",
|
||||
"selection_path": "rank_1",
|
||||
"selected_rank": 1,
|
||||
"selected_template_id": "pre_construction_model_info_stacked",
|
||||
"fallback_trigger": null
|
||||
},
|
||||
{
|
||||
"section_id": "04-2",
|
||||
"selection_path": "rank_2_fallback",
|
||||
"selected_rank": 2,
|
||||
"selected_template_id": "process_product_two_way",
|
||||
"fallback_trigger": "capacity_mismatch:strict_mismatch"
|
||||
}
|
||||
],
|
||||
"policy": "IMP-05: rank-1 is kept when usable; rank-2/3 may be promoted only when the earlier rank is not auto-renderable, has no catalog contract, or fails capacity precheck. calculate_fit is not used."
|
||||
},
|
||||
"frame_overrides_applied": [],
|
||||
"frame_overrides_skipped": [],
|
||||
"section_assignment_plan": null,
|
||||
"section_assignment_summary": null,
|
||||
"v0_lock_note": "Step 9 v0 passive (사용자 lock 2026-05-08). Step 6 default 그대로 사용 — runtime 결과 byte-동일. auto decision / scoring 은 Step 9 v1 (별 axis)."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"step_num": 20,
|
||||
"step_name": "slide_status",
|
||||
"step_status": "done",
|
||||
"pipeline_path_connected": true,
|
||||
"input": [
|
||||
"step14_visual_check.json",
|
||||
"step17_retry_trace.json",
|
||||
"step19_next_action.json"
|
||||
],
|
||||
"output": [
|
||||
"step20_slide_status.json",
|
||||
"step20_final_status.html"
|
||||
],
|
||||
"note": "자동 파이프라인 최종 결과 보고. overall = PASS/RENDERED_WITH_VISUAL_REGRESSION/PARTIAL_COVERAGE 등.",
|
||||
"data": {
|
||||
"rendered": true,
|
||||
"visual_check_passed": false,
|
||||
"full_mdx_coverage": true,
|
||||
"aligned_section_ids": [
|
||||
"04-1",
|
||||
"04-2"
|
||||
],
|
||||
"covered_section_ids": [
|
||||
"04-1",
|
||||
"04-2"
|
||||
],
|
||||
"filtered_section_ids": [],
|
||||
"filtered_section_reasons": [],
|
||||
"selection_path": "fallback_used",
|
||||
"fallback_used": true,
|
||||
"fallback_selections": [
|
||||
{
|
||||
"position": "bottom",
|
||||
"source_section_ids": [
|
||||
"04-2"
|
||||
],
|
||||
"template_id": "process_product_two_way",
|
||||
"selected_v4_rank": 2,
|
||||
"selection_path": "rank_2_fallback",
|
||||
"fallback_reason": "capacity_mismatch:strict_mismatch"
|
||||
}
|
||||
],
|
||||
"fallback_selection_count": 1,
|
||||
"selection_paths": [
|
||||
{
|
||||
"section_id": "04-1",
|
||||
"selection_path": "rank_1",
|
||||
"selected_rank": 1,
|
||||
"selected_template_id": "pre_construction_model_info_stacked",
|
||||
"fallback_trigger": null
|
||||
},
|
||||
{
|
||||
"section_id": "04-2",
|
||||
"selection_path": "rank_2_fallback",
|
||||
"selected_rank": 2,
|
||||
"selected_template_id": "process_product_two_way",
|
||||
"fallback_trigger": "capacity_mismatch:strict_mismatch"
|
||||
}
|
||||
],
|
||||
"visual_fail_reasons": [
|
||||
"zone--bottom: inner clipped .f29b — excess 17px vert / 0px horiz (content 377 vs container 360)",
|
||||
"zone--bottom: inner clipped .f29b__body — excess 17px vert / 0px horiz (content 311 vs container 294)"
|
||||
],
|
||||
"adapter_needed_count": 0,
|
||||
"adapter_needed_units": [],
|
||||
"content_truncated_count": 0,
|
||||
"content_truncated_units": [],
|
||||
"overall": "RENDERED_WITH_VISUAL_REGRESSION",
|
||||
"note": "자동 파이프라인 결과 보고. review/UI 개념 X. final.html 파일명 != PASS 의미. overall == PASS 는 visual OK + full coverage + adapter_needed=0 일 때만. adapter_needed_count > 0 = mapper 가 contract 와 안 맞아 자동 렌더 못 한 zone 존재. content_truncated_count > 0 = builder 가 truncate 한 zone 존재 (rendered 됐지만 일부 콘텐츠 손실)."
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,9 @@ export default function FramePanel({
|
||||
{candidates.length === 0 ? (
|
||||
<div className="py-20 text-center border-2 border-dashed border-slate-50 rounded-2xl">
|
||||
<p className="text-[10px] font-bold text-slate-300 uppercase">No Candidates Available</p>
|
||||
<p className="mt-2 px-4 text-[10px] leading-relaxed text-slate-400">
|
||||
backend candidate pool is empty for {assignedSectionIds.join(", ") || selectedZone.zone_id}.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
candidates.map((candidate, index) => {
|
||||
@@ -120,6 +123,7 @@ export default function FramePanel({
|
||||
candidate.routeHint && candidate.routeHint !== "direct_render";
|
||||
const showStatusChip =
|
||||
candidate.phaseZStatus && candidate.phaseZStatus !== "auto_renderable";
|
||||
const showCoverageChip = Boolean(candidate.coverageState);
|
||||
const hasCapacityFit =
|
||||
candidate.capacityFit && candidate.capacityFit.fit_status;
|
||||
const capacityMismatch =
|
||||
@@ -132,6 +136,10 @@ export default function FramePanel({
|
||||
if (candidate.routeHint) evidenceLines.push(`route: ${candidate.routeHint}`);
|
||||
if (candidate.phaseZStatus)
|
||||
evidenceLines.push(`phase_z_status: ${candidate.phaseZStatus}`);
|
||||
if (candidate.candidateStatus)
|
||||
evidenceLines.push(`candidate_status: ${candidate.candidateStatus}`);
|
||||
if (candidate.coverageState)
|
||||
evidenceLines.push(`coverage_state: ${candidate.coverageState}`);
|
||||
if (hasCapacityFit) {
|
||||
const cf = candidate.capacityFit!;
|
||||
const capacityLine =
|
||||
@@ -279,6 +287,28 @@ export default function FramePanel({
|
||||
{candidate.label}
|
||||
</span>
|
||||
)}
|
||||
{showCoverageChip && (
|
||||
<span
|
||||
className={`text-[8px] font-black uppercase tracking-tight px-1.5 py-0.5 rounded ${
|
||||
candidate.coverageState === "covered_native"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
: candidate.coverageState === "covered_via_expand"
|
||||
? "bg-cyan-50 text-cyan-700"
|
||||
: candidate.coverageState === "requires_adaptation"
|
||||
? "bg-amber-50 text-amber-700"
|
||||
: "bg-slate-100 text-slate-600"
|
||||
}`}
|
||||
title={`coverage_state: ${candidate.coverageState}`}
|
||||
>
|
||||
{candidate.coverageState === "covered_native"
|
||||
? "native"
|
||||
: candidate.coverageState === "covered_via_expand"
|
||||
? "expand"
|
||||
: candidate.coverageState === "requires_adaptation"
|
||||
? "adapt"
|
||||
: "unsup"}
|
||||
</span>
|
||||
)}
|
||||
{/* IMP-29 u3 — route hint chip (skip when direct_render = default). */}
|
||||
{showRouteChip && (
|
||||
<span
|
||||
|
||||
@@ -34,12 +34,14 @@ interface LeftMdxPanelProps {
|
||||
onFileUpload: (file: File) => void;
|
||||
onGenerate: () => void;
|
||||
onSectionClick: (sectionId: string) => void;
|
||||
/** 사용자 lock 2026-05-14 — 좌측 패널에 03/04/05 fix 고정 list. 클릭 시 callback. */
|
||||
onSelectSample?: (which: "03" | "04" | "05") => void;
|
||||
selectedSample?: "03" | "04" | "05" | null;
|
||||
/** 사용자 lock 2026-05-14 — 좌측 패널에 01~05 fix 고정 list. 클릭 시 callback. */
|
||||
onSelectSample?: (which: "01" | "02" | "03" | "04" | "05") => void;
|
||||
selectedSample?: "01" | "02" | "03" | "04" | "05" | null;
|
||||
}
|
||||
|
||||
const SAMPLE_MDX_LIST: { key: "03" | "04" | "05"; label: string; subtitle: string }[] = [
|
||||
const SAMPLE_MDX_LIST: { key: "01" | "02" | "03" | "04" | "05"; label: string; subtitle: string }[] = [
|
||||
{ key: "01", label: "01. 건설산업 DX의 올바른 이해", subtitle: "DX 개념 + 산업 전환 관점" },
|
||||
{ key: "02", label: "02. DX의 시행 목표 및 기대효과", subtitle: "시행 목표 + 기대효과" },
|
||||
{ key: "03", label: "03. DX 시행을 위한 필수 요건", subtitle: "필수 요건 + Process/Product 혁신" },
|
||||
{ key: "04", label: "04. DX 지연 요인", subtitle: "DX 인식 + 정책/조직 실태" },
|
||||
{ key: "05", label: "05. 설계 방식의 왜곡", subtitle: "설계 자동화 오용 + S/W 한계" },
|
||||
@@ -74,7 +76,7 @@ export default function LeftMdxPanel({
|
||||
MDX Source
|
||||
</h2>
|
||||
|
||||
{/* 2026-05-14 — 03/04/05 fix 고정 list. 클릭 시 해당 mdx 자동 fetch + 분석.
|
||||
{/* 2026-05-14 — 01~05 fix 고정 list. 클릭 시 해당 mdx 자동 fetch + 분석.
|
||||
frame/layout override 는 분석 후 우측 패널에서 가능. */}
|
||||
{onSelectSample && (
|
||||
<div className="space-y-1 mb-3">
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { AlertTriangle, CheckCircle2, GitBranch, Layers3 } from "lucide-react";
|
||||
import type React from "react";
|
||||
import type { AiTraceSummary, PipelineTraceSummary } from "../services/designAgentApi";
|
||||
|
||||
interface PipelineTracePanelProps {
|
||||
trace: PipelineTraceSummary | null | undefined;
|
||||
aiTrace?: AiTraceSummary | null;
|
||||
}
|
||||
|
||||
const stateClass: Record<string, string> = {
|
||||
rendered: "bg-emerald-50 text-emerald-700 border-emerald-200",
|
||||
render_blocked: "bg-red-50 text-red-700 border-red-200",
|
||||
filtered: "bg-amber-50 text-amber-700 border-amber-200",
|
||||
covered_not_rendered: "bg-orange-50 text-orange-700 border-orange-200",
|
||||
uncovered: "bg-slate-50 text-slate-500 border-slate-200",
|
||||
};
|
||||
|
||||
function Chip({ children, tone = "slate" }: { children: React.ReactNode; tone?: "slate" | "red" | "green" | "amber" }) {
|
||||
const tones = {
|
||||
slate: "bg-slate-100 text-slate-600",
|
||||
red: "bg-red-100 text-red-700",
|
||||
green: "bg-emerald-100 text-emerald-700",
|
||||
amber: "bg-amber-100 text-amber-700",
|
||||
};
|
||||
return (
|
||||
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[9px] font-black uppercase ${tones[tone]}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PipelineTracePanel({ trace, aiTrace }: PipelineTracePanelProps) {
|
||||
if (!trace) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-slate-50 p-8 text-center text-slate-400">
|
||||
<div>
|
||||
<GitBranch className="mx-auto mb-3 h-10 w-10 opacity-20" />
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em]">No Trace Loaded</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto bg-white p-4 text-slate-700">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-4 w-4 text-blue-500" />
|
||||
<h3 className="text-[10px] font-black uppercase tracking-widest text-slate-500">Pipeline Trace</h3>
|
||||
</div>
|
||||
<Chip tone={trace.warnings.length > 0 ? "amber" : "green"}>{trace.status}</Chip>
|
||||
</div>
|
||||
|
||||
{trace.warnings.length > 0 && (
|
||||
<div className="mb-4 rounded border border-amber-200 bg-amber-50 p-3">
|
||||
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-black uppercase text-amber-700">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
Warnings
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{trace.warnings.map((warning) => (
|
||||
<Chip key={warning} tone="amber">{warning}</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="mb-5">
|
||||
<div className="mb-2 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-slate-400">
|
||||
<Layers3 className="h-3.5 w-3.5" />
|
||||
Sections
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{trace.sections.map((section) => (
|
||||
<div key={section.id} className={`rounded border p-2 ${stateClass[section.state] ?? stateClass.uncovered}`}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="font-mono text-[11px] font-bold">{section.id}</span>
|
||||
<span className="text-[9px] font-black uppercase">{section.state}</span>
|
||||
</div>
|
||||
{section.title && <div className="mt-1 text-[11px] leading-snug">{section.title}</div>}
|
||||
{section.child_ids.length > 0 && (
|
||||
<div className="mt-1 text-[10px] text-slate-500">children: {section.child_ids.join(", ")}</div>
|
||||
)}
|
||||
{section.reasons.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{section.reasons.map((reason) => (
|
||||
<Chip key={reason} tone={section.state === "render_blocked" ? "red" : "amber"}>
|
||||
{reason}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-5">
|
||||
<div className="mb-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Units</div>
|
||||
<div className="space-y-2">
|
||||
{trace.units.map((unit) => (
|
||||
<div key={unit.unit_id} className="rounded border border-slate-200 bg-slate-50 p-2">
|
||||
<div className="font-mono text-[11px] font-bold text-slate-700">{unit.unit_id || "(empty unit)"}</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-x-2 gap-y-1 text-[10px]">
|
||||
<span>frame</span><span className="font-mono">{unit.selected_frame ?? "-"}</span>
|
||||
<span>candidates</span><span className="font-mono">{unit.candidate_count}</span>
|
||||
<span>path</span><span className="font-mono">{unit.selection_path ?? "-"}</span>
|
||||
<span>label</span><span className="font-mono">{unit.label ?? "-"}</span>
|
||||
</div>
|
||||
{unit.warnings.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{unit.warnings.map((warning) => <Chip key={warning} tone="amber">{warning}</Chip>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-2 text-[10px] font-black uppercase tracking-widest text-slate-400">Zones / Render</div>
|
||||
<div className="space-y-2">
|
||||
{trace.zones.map((zone) => {
|
||||
const filled = zone.slot_status === "filled";
|
||||
return (
|
||||
<div key={zone.position} className="rounded border border-slate-200 bg-white p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-[11px] font-bold">{zone.position}</span>
|
||||
<Chip tone={filled ? "green" : "red"}>{zone.slot_status}</Chip>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] text-slate-500">sections: {zone.source_section_ids.join(", ") || "-"}</div>
|
||||
<div className="mt-1 text-[10px] text-slate-500">template: <span className="font-mono">{zone.template_id ?? "-"}</span></div>
|
||||
<div className="mt-1 text-[10px] text-slate-500">slot keys: {zone.slot_key_count}</div>
|
||||
{zone.warnings.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{zone.warnings.map((warning) => <Chip key={warning} tone="red">{warning}</Chip>)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 flex items-center gap-1 text-[10px] text-emerald-600">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
render payload ready
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-5">
|
||||
<div className="mb-2 text-[10px] font-black uppercase tracking-widest text-slate-400">AI Status</div>
|
||||
{!aiTrace ? (
|
||||
<div className="rounded border border-slate-200 bg-slate-50 p-3 text-[11px] text-slate-500">
|
||||
AI trace not loaded.
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded border border-slate-200 bg-white p-3">
|
||||
<div className="mb-2 flex flex-wrap gap-1">
|
||||
<Chip tone={aiTrace.artifact_present ? "green" : "red"}>
|
||||
{aiTrace.artifact_present ? "artifact" : "missing"}
|
||||
</Chip>
|
||||
<Chip tone={aiTrace.ai_enabled === false ? "amber" : aiTrace.ai_enabled ? "green" : "slate"}>
|
||||
enabled {aiTrace.ai_enabled === null ? "unknown" : String(aiTrace.ai_enabled)}
|
||||
</Chip>
|
||||
<Chip tone={aiTrace.ai_called_count > 0 ? "green" : "slate"}>
|
||||
called {aiTrace.ai_called_count}
|
||||
</Chip>
|
||||
<Chip tone={aiTrace.eligible_count > 0 ? "amber" : "slate"}>
|
||||
eligible {aiTrace.eligible_count}
|
||||
</Chip>
|
||||
<Chip tone={aiTrace.error_count > 0 ? "red" : "slate"}>
|
||||
errors {aiTrace.error_count}
|
||||
</Chip>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-[10px] text-slate-500">
|
||||
<span>status</span><span className="font-mono">{aiTrace.status}</span>
|
||||
<span>coverage</span><span className="font-mono">{aiTrace.coverage_status ?? "-"}</span>
|
||||
<span>provider</span><span className="font-mono">{aiTrace.provider ?? "-"}</span>
|
||||
<span>human review</span><span className="font-mono">{String(aiTrace.human_review_required)}</span>
|
||||
</div>
|
||||
{Object.keys(aiTrace.skip_reasons).length > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="mb-1 text-[10px] font-black uppercase text-slate-400">Skip Reasons</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Object.entries(aiTrace.skip_reasons).map(([reason, count]) => (
|
||||
<Chip key={reason} tone={reason === "called" ? "green" : "slate"}>
|
||||
{reason}: {count}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{aiTrace.units.length > 0 && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{aiTrace.units.map((unit) => (
|
||||
<div key={`${unit.unit_index}-${unit.source_section_ids.join("+")}`} className="rounded bg-slate-50 p-2 text-[10px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono font-bold">{unit.source_section_ids.join("+") || `unit-${unit.unit_index}`}</span>
|
||||
<Chip tone={unit.ai_called ? "green" : "slate"}>{unit.ai_called ? "called" : "not called"}</Chip>
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-x-2 gap-y-1 text-slate-500">
|
||||
<span>route</span><span className="font-mono">{unit.route_hint ?? "-"}</span>
|
||||
<span>skip</span><span className="font-mono">{unit.skip_reason ?? "-"}</span>
|
||||
<span>apply</span><span className="font-mono">{unit.apply_status ?? "-"}</span>
|
||||
<span>error</span><span className="font-mono">{unit.error ?? "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Design Agent - Mock Data
|
||||
*
|
||||
* 이 파일은 백엔드 파이프라인이 연결되기 전까지 사용하는 mock 데이터입니다.
|
||||
* Phase Z 설계 규격을 따릅니다.
|
||||
*/
|
||||
|
||||
import type {
|
||||
NormalizedContent,
|
||||
SlidePlan,
|
||||
LayoutCandidate,
|
||||
FrameCandidate,
|
||||
} from "../types/designAgent";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Layout Candidates
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// 8 preset = backend templates/phase_z2/layouts/layouts.yaml 1:1 매칭.
|
||||
// id / 순서 / 의미 모두 backend 와 동일해야 함.
|
||||
export const MOCK_LAYOUT_CANDIDATES: LayoutCandidate[] = [
|
||||
{ id: "single", name: "단일 본문", type: "full", description: "전체 영역 1 zone (primary)" },
|
||||
{ id: "horizontal-2", name: "상/하 2단", type: "top-bottom", description: "위/아래 2 zone (top, bottom) — rows topology" },
|
||||
{ id: "vertical-2", name: "좌/우 2단", type: "left-right", description: "좌/우 2 zone (left, right) — cols topology" },
|
||||
{ id: "top-1-bottom-2", name: "상 1 : 하 2", type: "asymmetric", description: "위 1 zone + 아래 좌/우 2 zone — T topology" },
|
||||
{ id: "top-2-bottom-1", name: "상 2 : 하 1", type: "asymmetric", description: "위 좌/우 2 zone + 아래 1 zone — inverted-T topology" },
|
||||
{ id: "left-1-right-2", name: "좌 1 : 우 2", type: "asymmetric", description: "좌 1 zone + 우 상/하 2 zone — side-T-left topology" },
|
||||
{ id: "left-2-right-1", name: "좌 2 : 우 1", type: "asymmetric", description: "좌 상/하 2 zone + 우 1 zone — side-T-right topology" },
|
||||
{ id: "grid-2x2", name: "2x2 그리드", type: "grid", description: "4 zone 균등 배치 — 2x2 topology" },
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Frame Candidates (샘플 이미지 포함 - 8종 세트)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const SAMPLE_FRAMES: FrameCandidate[] = [
|
||||
{
|
||||
id: "frame-001",
|
||||
name: "3-Card 프로세스",
|
||||
score: 0.98,
|
||||
confidence: "high",
|
||||
label: "use_as_is",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1614850523296-d8c1af93d400?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-002",
|
||||
name: "비교 테이블 (Light)",
|
||||
score: 0.85,
|
||||
confidence: "medium",
|
||||
label: "light_edit",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1508615039623-a25651266b91?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-003",
|
||||
name: "계층형 트리",
|
||||
score: 0.72,
|
||||
confidence: "medium",
|
||||
label: "restructure",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1551288049-bbda03a24d5d?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-004",
|
||||
name: "4-Grid 핵심 요약",
|
||||
score: 0.65,
|
||||
confidence: "low",
|
||||
label: "reject",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-005",
|
||||
name: "타임라인 로드맵",
|
||||
score: 0.91,
|
||||
confidence: "high",
|
||||
label: "use_as_is",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1531403009284-440f080d1e12?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-006",
|
||||
name: "순환형 다이어그램",
|
||||
score: 0.78,
|
||||
confidence: "medium",
|
||||
label: "light_edit",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1557804506-669a67965ba0?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-007",
|
||||
name: "수치 강조 카드",
|
||||
score: 0.88,
|
||||
confidence: "high",
|
||||
label: "use_as_is",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1551288049-bbda03a24d5d?w=300&h=200&fit=crop",
|
||||
},
|
||||
{
|
||||
id: "frame-008",
|
||||
name: "좌우 대비 분석",
|
||||
score: 0.62,
|
||||
confidence: "low",
|
||||
label: "restructure",
|
||||
thumbnailUrl: "https://images.unsplash.com/photo-1454165833767-02a6ed8a687a?w=300&h=200&fit=crop",
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_FRAME_CANDIDATES_SECTION1 = SAMPLE_FRAMES;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Normalized Content
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const MOCK_NORMALIZED_CONTENT: NormalizedContent = {
|
||||
title: "DX 실행 체계 구축 방안",
|
||||
sections: [
|
||||
{
|
||||
id: "section-1",
|
||||
index: 1,
|
||||
level: 2,
|
||||
title: "DX 실행 필수 요건",
|
||||
content_objects: [
|
||||
{ id: "co-1", type: "text_block", role: "summary", raw_payload: "조직 전반의 DX 역량 강화 필요", size_estimate: { line_count: 2 } },
|
||||
{ id: "co-2", type: "text_block", role: "detail", raw_payload: "전담 조직 구성 및 KPI 설정", size_estimate: { line_count: 3 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Slide Plan (Phase Z 구조)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const MOCK_SLIDE_PLAN: SlidePlan = {
|
||||
id: "slide-001",
|
||||
title: "DX 실행 체계 구축 방안",
|
||||
layout_preset: "horizontal-2",
|
||||
zones: [
|
||||
{
|
||||
id: "zone-left",
|
||||
zone_id: "left",
|
||||
section_ids: ["section-1"],
|
||||
position: { x: 0, y: 0, width: 0.5, height: 1 },
|
||||
internal_regions: [
|
||||
{
|
||||
id: "region-l1",
|
||||
region_id: "content-1",
|
||||
role: "primary",
|
||||
content_type: "mixed",
|
||||
ratio_estimate: 1,
|
||||
content_unit_ids: ["co-1", "co-2"],
|
||||
frame_match_strategy: {
|
||||
kind: "frame_match",
|
||||
frame_id: "frame-001",
|
||||
display_strategy: "inline_full"
|
||||
},
|
||||
frame_candidates: SAMPLE_FRAMES
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "zone-right",
|
||||
zone_id: "right",
|
||||
section_ids: ["section-1"],
|
||||
position: { x: 0.5, y: 0, width: 0.5, height: 1 },
|
||||
internal_regions: [
|
||||
{
|
||||
id: "region-r1",
|
||||
region_id: "content-2",
|
||||
role: "secondary",
|
||||
content_type: "text_block",
|
||||
ratio_estimate: 1,
|
||||
content_unit_ids: [],
|
||||
frame_match_strategy: {
|
||||
kind: "frame_match",
|
||||
frame_id: "frame-002",
|
||||
display_strategy: "inline_full"
|
||||
},
|
||||
frame_candidates: SAMPLE_FRAMES
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
saveStructureOverride,
|
||||
deriveUserOverridesKey,
|
||||
applyPersistedNonFrameOverrides,
|
||||
mergeSubmittedPipelineOverridesForRestore,
|
||||
remapPersistedFramesToZoneFrames,
|
||||
validateZoneGeometriesAgainstLayout,
|
||||
} from "../utils/slidePlanUtils";
|
||||
@@ -43,6 +44,7 @@ import LeftMdxPanel from "../components/LeftMdxPanel";
|
||||
import SlideCanvas from "../components/SlideCanvas";
|
||||
import LayoutPanel from "../components/LayoutPanel";
|
||||
import FramePanel from "../components/FramePanel";
|
||||
import PipelineTracePanel from "../components/PipelineTracePanel";
|
||||
import BottomActions from "../components/BottomActions";
|
||||
import {
|
||||
Sparkles, Loader2,
|
||||
@@ -60,7 +62,7 @@ const INITIAL_STATE: DesignAgentState = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
type RightPanelTab = "layout" | "frame";
|
||||
type RightPanelTab = "layout" | "frame" | "trace";
|
||||
|
||||
export default function Home() {
|
||||
const [state, setState] = useState<DesignAgentState>(INITIAL_STATE);
|
||||
@@ -266,7 +268,10 @@ export default function Home() {
|
||||
setState((p) => ({
|
||||
...p,
|
||||
normalizedContent: content,
|
||||
userSelection: applyPersistedNonFrameOverrides(p.userSelection, persisted),
|
||||
// #98 Task 1 — selecting/uploading a MDX starts from a clean analysis
|
||||
// state. Persisted overrides stay cached for explicit edit/regenerate
|
||||
// paths, but they must not pre-seed the first "Generate" run.
|
||||
userSelection: createInitialUserSelection(),
|
||||
isLoading: false,
|
||||
}));
|
||||
toast.success(`"${file.name}" 분석 완료 — 하단 버튼으로 슬라이드 생성하세요.`);
|
||||
@@ -279,11 +284,11 @@ export default function Home() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 2026-05-14 — 좌측 패널의 03/04/05 fix list 클릭 또는 URL `?mdx=04` 변경 시
|
||||
// 2026-05-14 — 좌측 패널의 01~05 fix list 클릭 또는 URL `?mdx=04` 변경 시
|
||||
// 호출되는 단일 callback. handleFileUpload 가 자동 분석 trigger.
|
||||
const [selectedSample, setSelectedSample] = useState<"03" | "04" | "05" | null>(null);
|
||||
const [selectedSample, setSelectedSample] = useState<"01" | "02" | "03" | "04" | "05" | null>(null);
|
||||
|
||||
const handleSelectSample = useCallback(async (which: "03" | "04" | "05") => {
|
||||
const handleSelectSample = useCallback(async (which: "01" | "02" | "03" | "04" | "05") => {
|
||||
try {
|
||||
const res = await fetch(`/api/sample-mdx?mdx=${encodeURIComponent(which)}`);
|
||||
if (!res.ok) return;
|
||||
@@ -304,11 +309,11 @@ export default function Home() {
|
||||
}, []);
|
||||
|
||||
// 페이지 첫 로드 시 데모용 mdx 자동 로드 — 상대방에게 mdx 파일 공유 안 해도 되게.
|
||||
// URL query `?mdx=04` / `?mdx=05` 로 다른 sample 선택 가능. default = 03.
|
||||
// URL query `?mdx=01`~`?mdx=05` 로 다른 sample 선택 가능. default = 03.
|
||||
// 사용자가 다른 파일을 직접 업로드하면 그것이 override 됨.
|
||||
useEffect(() => {
|
||||
const which = (new URLSearchParams(window.location.search).get("mdx") as "03" | "04" | "05" | null) || "03";
|
||||
if (!["03", "04", "05"].includes(which)) return;
|
||||
const which = (new URLSearchParams(window.location.search).get("mdx") as "01" | "02" | "03" | "04" | "05" | null) || "03";
|
||||
if (!["01", "02", "03", "04", "05"].includes(which)) return;
|
||||
handleSelectSample(which);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
@@ -326,9 +331,10 @@ export default function Home() {
|
||||
// - layout : userSelection.overrides.layout_preset (default 와 다를 때만)
|
||||
// - frames : zone.section_ids → unit_id ("+".join). region.id 별 zone_frames lookup.
|
||||
// pendingZones 의 region 도 동일 — 그 region 의 zone 의 sections 가 unit_id 결정.
|
||||
const shouldUseUserOverrides = hasPendingChanges;
|
||||
const overrides: PipelineOverrides = {};
|
||||
const sourcePlan = effectiveSlidePlan;
|
||||
if (sourcePlan && state.slidePlan) {
|
||||
if (shouldUseUserOverrides && sourcePlan && state.slidePlan) {
|
||||
// 2026-05-22 demo hot-fix — 이전 비교 가드 (default !== override) 제거.
|
||||
// restore loop 이 default = override 로 sync 시 override 안 보내고 backend
|
||||
// default fallback 발생. user 가 명시한 layout 이 있으면 무조건 보냄.
|
||||
@@ -438,6 +444,11 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
// #98 Task 4 — stale render guard. Once a new generate/regenerate starts,
|
||||
// the previous final.html must no longer be treated as the current result.
|
||||
// If the backend run later fails, keeping the old runMeta would resurrect
|
||||
// the old iframe and make it look as if the new selection rendered.
|
||||
setRunMeta(null);
|
||||
setState((p) => ({ ...p, isLoading: true }));
|
||||
setHasPendingChanges(false); // 재생성 트리거 시 override pending flag reset
|
||||
setPendingLayout(null); // pending layout 모드 종료
|
||||
@@ -461,6 +472,16 @@ export default function Home() {
|
||||
// clicks Generate would race the PUT against /api/run; the u2
|
||||
// fallback could then load a stale persisted document.
|
||||
await flushUserOverrides();
|
||||
const restoreOverrides = shouldUseUserOverrides
|
||||
? mergeSubmittedPipelineOverridesForRestore(
|
||||
await getUserOverrides(deriveUserOverridesKey(state.uploadedFile.name)),
|
||||
overrides,
|
||||
state.userSelection.overrides.manual_section_assignment,
|
||||
)
|
||||
: {};
|
||||
if (shouldUseUserOverrides) {
|
||||
persistedOverridesRef.current = restoreOverrides;
|
||||
}
|
||||
// IMP-42 u4 — unconditional DIAG console.log on the handleGenerate
|
||||
// entry-to-backend boundary. Surfaces the override payload + uploaded
|
||||
// file name so the user can see exactly what crossed the wire when
|
||||
@@ -469,7 +490,12 @@ export default function Home() {
|
||||
file: state.uploadedFile.name,
|
||||
overrides,
|
||||
});
|
||||
const result = await runPipeline(state.uploadedFile, overrides);
|
||||
const result = await runPipeline(
|
||||
state.uploadedFile,
|
||||
shouldUseUserOverrides ? overrides : undefined,
|
||||
undefined,
|
||||
{ ignoreUserOverrides: !shouldUseUserOverrides },
|
||||
);
|
||||
|
||||
if (!result.success || !result.final_html_exists) {
|
||||
const detail =
|
||||
@@ -488,10 +514,12 @@ export default function Home() {
|
||||
// zone_frames is keyed by region.id. Remap against the new slidePlan
|
||||
// zones so SlideCanvas's override-vs-default preview indicator shows
|
||||
// the user's persisted choice without forcing them to re-click.
|
||||
const restoredZoneFrames = remapPersistedFramesToZoneFrames(
|
||||
slidePlan,
|
||||
persistedOverridesRef.current.frames as Record<string, string> | undefined,
|
||||
);
|
||||
const restoredZoneFrames = shouldUseUserOverrides
|
||||
? remapPersistedFramesToZoneFrames(
|
||||
slidePlan,
|
||||
restoreOverrides.frames as Record<string, string> | undefined,
|
||||
)
|
||||
: {};
|
||||
setState((p) => {
|
||||
// IMP-52 u6 — restore-on-reopen: re-layer the persisted non-frame
|
||||
// axes (layout / zone_geometries / zone_sections) onto the post-load
|
||||
@@ -500,10 +528,10 @@ export default function Home() {
|
||||
// a CLI arg — `zone_geometries` in particular has no slidePlan
|
||||
// representation, so without this merge the user would see their
|
||||
// resized zones revert on every Generate.
|
||||
const base = applyPersistedNonFrameOverrides(
|
||||
createInitialUserSelection(slidePlan),
|
||||
persistedOverridesRef.current,
|
||||
);
|
||||
const cleanBase = createInitialUserSelection(slidePlan);
|
||||
const base = shouldUseUserOverrides
|
||||
? applyPersistedNonFrameOverrides(cleanBase, restoreOverrides)
|
||||
: cleanBase;
|
||||
return {
|
||||
...p,
|
||||
normalizedContent,
|
||||
@@ -519,6 +547,9 @@ export default function Home() {
|
||||
};
|
||||
});
|
||||
setRunMeta(runMeta);
|
||||
if (runMeta.status !== "PASS" || (runMeta.pipeline_trace?.warnings.length ?? 0) > 0) {
|
||||
setRightTab("trace");
|
||||
}
|
||||
toast.success(`run "${result.run_id}" 완료 — ${runMeta.status}`);
|
||||
const aiReviewMsg = formatAiRepairHumanReviewMessage(runMeta.ai_repair_status);
|
||||
if (aiReviewMsg) toast.error(aiReviewMsg);
|
||||
@@ -529,7 +560,7 @@ export default function Home() {
|
||||
);
|
||||
setState((p) => ({ ...p, isLoading: false }));
|
||||
}
|
||||
}, [state.uploadedFile, state.slidePlan, state.userSelection, pendingZones, pendingLayout]);
|
||||
}, [state.uploadedFile, state.slidePlan, state.userSelection, pendingZones, pendingLayout, hasPendingChanges]);
|
||||
|
||||
// ── 섹션 드래그 앤 드롭 (Zone으로 재배치) ──
|
||||
const handleSectionDrop = useCallback((sectionId: string, zoneId: string) => {
|
||||
@@ -848,6 +879,16 @@ export default function Home() {
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
{(runMeta.pipeline_trace?.warnings.length ?? 0) > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-[10px] font-bold px-1.5 py-0.5 bg-red-100 text-red-700 rounded uppercase tracking-wider"
|
||||
onClick={() => setRightTab("trace")}
|
||||
title={runMeta.pipeline_trace?.warnings.join(" / ")}
|
||||
>
|
||||
Trace: {runMeta.pipeline_trace?.warnings.length}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -898,7 +939,7 @@ export default function Home() {
|
||||
slidePlan={effectiveSlidePlan}
|
||||
normalizedContent={state.normalizedContent}
|
||||
userSelection={state.userSelection}
|
||||
finalHtmlUrl={runMeta?.final_html_url}
|
||||
finalHtmlUrl={state.isLoading ? undefined : runMeta?.final_html_url}
|
||||
isPipelineRunning={state.isLoading}
|
||||
isPendingLayout={!!pendingLayout}
|
||||
pendingLayoutId={pendingLayout}
|
||||
@@ -927,6 +968,7 @@ export default function Home() {
|
||||
<div className="flex-shrink-0 flex p-1 bg-slate-50 border-b border-slate-200 m-2 rounded-lg">
|
||||
<button className={`flex-1 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-md transition-all ${rightTab === "frame" ? "bg-white text-blue-600 shadow-sm" : "text-slate-400"}`} onClick={() => setRightTab("frame")}>Frame</button>
|
||||
<button className={`flex-1 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-md transition-all ${rightTab === "layout" ? "bg-white text-blue-600 shadow-sm" : "text-slate-400"}`} onClick={() => setRightTab("layout")}>Layout</button>
|
||||
<button className={`flex-1 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-md transition-all ${rightTab === "trace" ? "bg-white text-blue-600 shadow-sm" : "text-slate-400"}`} onClick={() => setRightTab("trace")}>Trace</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{rightTab === "frame" ? (
|
||||
@@ -938,7 +980,7 @@ export default function Home() {
|
||||
onFrameSelect={handleFrameSelect}
|
||||
onNoDesignToggle={() => {}}
|
||||
/>
|
||||
) : (
|
||||
) : rightTab === "layout" ? (
|
||||
<LayoutPanel
|
||||
selectedZone={selectedZone}
|
||||
userSelection={state.userSelection}
|
||||
@@ -948,6 +990,8 @@ export default function Home() {
|
||||
pipelineSelectedLayoutId={state.slidePlan?.layout_preset}
|
||||
pendingLayoutId={pendingLayout}
|
||||
/>
|
||||
) : (
|
||||
<PipelineTracePanel trace={runMeta?.pipeline_trace} aiTrace={runMeta?.ai_trace} />
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -276,6 +276,74 @@ export interface RunMeta {
|
||||
region_layout_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
||||
display_strategy_candidates_by_zone: Record<string, string[]>; // step08 placeholder
|
||||
ai_repair_status: AiRepairStatus | null;
|
||||
pipeline_trace: PipelineTraceSummary | null;
|
||||
ai_trace: AiTraceSummary | null;
|
||||
}
|
||||
|
||||
export interface PipelineTraceSection {
|
||||
id: string;
|
||||
title: string;
|
||||
child_ids: string[];
|
||||
state: "rendered" | "render_blocked" | "filtered" | "covered_not_rendered" | "uncovered";
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
export interface PipelineTraceUnit {
|
||||
unit_id: string;
|
||||
source_section_ids: string[];
|
||||
merge_type: string | null;
|
||||
selected_frame: string | null;
|
||||
label: string | null;
|
||||
candidate_count: number;
|
||||
selection_path: string | null;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface PipelineTraceZone {
|
||||
position: string;
|
||||
source_section_ids: string[];
|
||||
template_id: string | null;
|
||||
slot_status: "filled" | "empty";
|
||||
slot_key_count: number;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface PipelineTraceSummary {
|
||||
sections: PipelineTraceSection[];
|
||||
units: PipelineTraceUnit[];
|
||||
zones: PipelineTraceZone[];
|
||||
status: string;
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface AiTraceUnit {
|
||||
unit_index: number | null;
|
||||
source_section_ids: string[];
|
||||
frame_template_id: string | null;
|
||||
route_hint: string | null;
|
||||
provisional: boolean;
|
||||
ai_called: boolean;
|
||||
skip_reason: string | null;
|
||||
apply_status: string | null;
|
||||
api_error_kind: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface AiTraceSummary {
|
||||
artifact_present: boolean;
|
||||
ai_enabled: boolean | null;
|
||||
status: string;
|
||||
ai_called_count: number;
|
||||
eligible_count: number;
|
||||
skipped_count: number;
|
||||
error_count: number;
|
||||
skip_reasons: Record<string, number>;
|
||||
provider: string | null;
|
||||
model: string | null;
|
||||
coverage_status: string | null;
|
||||
human_review_required: boolean;
|
||||
units: AiTraceUnit[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
// IMP-92 u5 — Operational-only AI repair message formatter.
|
||||
@@ -352,6 +420,7 @@ export async function runPipeline(
|
||||
// from the prior run). When omitted / empty, the POST body is
|
||||
// byte-identical to pre-u6 (no reuseFromRunId key → no flag forwarded).
|
||||
reuseFromRunId?: string,
|
||||
options?: { ignoreUserOverrides?: boolean },
|
||||
): Promise<RunPipelineResult> {
|
||||
const content = await file.text();
|
||||
const body: Record<string, unknown> = {
|
||||
@@ -360,6 +429,7 @@ export async function runPipeline(
|
||||
overrides,
|
||||
};
|
||||
if (reuseFromRunId) body.reuseFromRunId = reuseFromRunId;
|
||||
if (options?.ignoreUserOverrides) body.ignoreUserOverrides = true;
|
||||
const res = await fetch("/api/run", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -439,12 +509,417 @@ function classifyConfidence(score: number): "high" | "medium" | "low" {
|
||||
return "low";
|
||||
}
|
||||
|
||||
const TOP_N_FRAMES = 6;
|
||||
|
||||
function getCandidateTemplateId(c: any): string | null {
|
||||
const id = c?.template_id ?? c?.frame_template_id ?? c?.id;
|
||||
return typeof id === "string" && id.length > 0 ? id : null;
|
||||
}
|
||||
|
||||
function normalizeRawCandidate(c: any): any | null {
|
||||
const templateId = getCandidateTemplateId(c);
|
||||
if (!templateId) return null;
|
||||
return {
|
||||
...c,
|
||||
template_id: templateId,
|
||||
label: c.label ?? c.v4_label,
|
||||
confidence: c.confidence ?? c.score,
|
||||
rank: c.rank ?? c.v4_rank,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeMissingCandidateFields(existing: any, incoming: any): any {
|
||||
const merged = { ...existing };
|
||||
for (const [key, value] of Object.entries(incoming)) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
if (merged[key] === undefined || merged[key] === null || merged[key] === "") {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function pushCandidate(candidateMap: Map<string, any>, c: any): void {
|
||||
const normalized = normalizeRawCandidate(c);
|
||||
if (!normalized) return;
|
||||
const key = normalized.template_id;
|
||||
const prior = candidateMap.get(key);
|
||||
candidateMap.set(key, prior ? mergeMissingCandidateFields(prior, normalized) : normalized);
|
||||
}
|
||||
|
||||
function candidatesMatchingTemplate(unit: any, templateId: string | null): any[] {
|
||||
if (!templateId) return [];
|
||||
const sources = [
|
||||
unit?.sorted_candidate_evidence,
|
||||
unit?.candidate_evidence,
|
||||
unit?.v4_candidates,
|
||||
unit?.v4_all_judgments,
|
||||
unit?.application_candidates,
|
||||
];
|
||||
return sources
|
||||
.flatMap((source) => (Array.isArray(source) ? source : []))
|
||||
.filter((c) => getCandidateTemplateId(c) === templateId);
|
||||
}
|
||||
|
||||
function compositionUnitToCandidate(compositionUnit: any | null | undefined): any | null {
|
||||
if (!compositionUnit) return null;
|
||||
const templateId = getCandidateTemplateId(compositionUnit);
|
||||
if (!templateId) return null;
|
||||
return {
|
||||
template_id: templateId,
|
||||
frame_id: compositionUnit.frame_id,
|
||||
frame_number: compositionUnit.frame_number,
|
||||
confidence: compositionUnit.confidence ?? compositionUnit.score ?? 0,
|
||||
label: compositionUnit.label ?? "reject",
|
||||
v4_label: compositionUnit.label,
|
||||
phase_z_status: compositionUnit.phase_z_status,
|
||||
candidate_status: compositionUnit.candidate_status,
|
||||
coverage_state:
|
||||
compositionUnit.coverage_state ??
|
||||
(compositionUnit.rationale?.ai_adaptation_required === true
|
||||
? "requires_adaptation"
|
||||
: undefined),
|
||||
route_hint:
|
||||
compositionUnit.rationale?.ai_adaptation_required === true
|
||||
? "ai_adaptation_required"
|
||||
: undefined,
|
||||
decision: "selected",
|
||||
reason: compositionUnit.selection_path ?? compositionUnit.fallback_reason,
|
||||
capacity_fit: compositionUnit.rationale?.capacity_fit,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFrameCandidatesForUnit(
|
||||
unit: any,
|
||||
compositionUnit?: any | null,
|
||||
): FrameCandidate[] {
|
||||
const candidateMap = new Map<string, any>();
|
||||
const currentDefault =
|
||||
typeof unit?.current_default_candidate === "string"
|
||||
? unit.current_default_candidate
|
||||
: null;
|
||||
|
||||
candidatesMatchingTemplate(unit, currentDefault).forEach((c) => pushCandidate(candidateMap, c));
|
||||
pushCandidate(candidateMap, compositionUnitToCandidate(compositionUnit));
|
||||
|
||||
const sortedCandidateEvidence = Array.isArray(unit?.sorted_candidate_evidence)
|
||||
? unit.sorted_candidate_evidence
|
||||
: [];
|
||||
const candidateEvidence = Array.isArray(unit?.candidate_evidence)
|
||||
? unit.candidate_evidence
|
||||
: [];
|
||||
const primaryEvidence =
|
||||
sortedCandidateEvidence.length > 0 ? sortedCandidateEvidence : candidateEvidence;
|
||||
|
||||
primaryEvidence.forEach((c: any) => pushCandidate(candidateMap, c));
|
||||
(Array.isArray(unit?.v4_candidates) ? unit.v4_candidates : []).forEach((c: any) => pushCandidate(candidateMap, c));
|
||||
(Array.isArray(unit?.v4_all_judgments) ? unit.v4_all_judgments : []).forEach((c: any) => pushCandidate(candidateMap, c));
|
||||
(Array.isArray(unit?.application_candidates) ? unit.application_candidates : []).forEach((c: any) => pushCandidate(candidateMap, c));
|
||||
|
||||
const applicationModeMap = mergeApplicationCandidates(unit?.application_candidates);
|
||||
|
||||
return Array.from(candidateMap.values())
|
||||
.slice(0, TOP_N_FRAMES)
|
||||
.map((c: any) => {
|
||||
const appMatch = applicationModeMap.get(c.template_id);
|
||||
const numericScore = Number(c.confidence ?? c.score ?? 0);
|
||||
const frameNumber = Number(c.frame_number);
|
||||
return {
|
||||
id: c.template_id,
|
||||
name: c.template_id,
|
||||
score: Number.isFinite(numericScore) ? numericScore : 0,
|
||||
confidence: classifyConfidence(Number.isFinite(numericScore) ? numericScore : 0),
|
||||
label: (c.label ?? c.v4_label ?? "reject") as FrameCandidate["label"],
|
||||
thumbnailUrl:
|
||||
Number.isFinite(frameNumber) && frameNumber > 0
|
||||
? `/frame-preview/${String(frameNumber).padStart(2, "0")}`
|
||||
: undefined,
|
||||
catalogRegistered: c.catalog_registered,
|
||||
minHeightPx: c.min_height_px ?? undefined,
|
||||
rank: c.rank,
|
||||
frameId: c.frame_id,
|
||||
v4Label: c.v4_label,
|
||||
phaseZStatus: c.phase_z_status,
|
||||
coverageState: c.coverage_state ?? c.coverageState,
|
||||
candidateStatus: c.candidate_status ?? c.candidateStatus,
|
||||
filteredForDirectExecution: c.filtered_for_direct_execution,
|
||||
routeHint: c.route_hint,
|
||||
decision: c.decision,
|
||||
reason: c.reason,
|
||||
capacityFit: c.capacity_fit,
|
||||
applicationMode: appMatch?.application_mode ?? c.application_mode,
|
||||
autoApplicable: appMatch?.auto_applicable ?? c.auto_applicable,
|
||||
delegatedTo: appMatch?.delegated_to ?? c.delegated_to ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 Phase Z run 산출물을 로드하여 frontend type 으로 변환.
|
||||
*
|
||||
* @example
|
||||
* const { normalizedContent, slidePlan, runMeta } = await loadRun("mdx03_f29_fix_check");
|
||||
*/
|
||||
function arrayOfStrings(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
|
||||
}
|
||||
|
||||
function slotPayloadStatus(zone: any): { status: "filled" | "empty"; keyCount: number } {
|
||||
const payload = zone?.slot_payload;
|
||||
const keys = payload && typeof payload === "object" && !Array.isArray(payload)
|
||||
? Object.keys(payload).filter((key) => key !== "_truncated_count")
|
||||
: [];
|
||||
const templateId = zone?.template_id ?? zone?.v4_template_id ?? null;
|
||||
return {
|
||||
status: templateId === "__empty__" || keys.length === 0 ? "empty" : "filled",
|
||||
keyCount: keys.length,
|
||||
};
|
||||
}
|
||||
|
||||
function pushReason(map: Map<string, string[]>, id: string, reason: string): void {
|
||||
if (!id || !reason) return;
|
||||
const reasons = map.get(id) ?? [];
|
||||
if (!reasons.includes(reason)) reasons.push(reason);
|
||||
map.set(id, reasons);
|
||||
}
|
||||
|
||||
function formatFilteredReason(reason: any): string {
|
||||
const parts = [
|
||||
reason?.selection_state,
|
||||
reason?.merge_type,
|
||||
reason?.template_id,
|
||||
reason?.v4_label,
|
||||
reason?.phase_z_status,
|
||||
...(Array.isArray(reason?.filter_reasons) ? reason.filter_reasons : []),
|
||||
].filter((value): value is string => typeof value === "string" && value.length > 0);
|
||||
return parts.join(" / ") || "filtered";
|
||||
}
|
||||
|
||||
export function buildPipelineTraceSummary(args: {
|
||||
normalized: any;
|
||||
compositionPlan: any;
|
||||
applicationPlan: any;
|
||||
slotPayload: any;
|
||||
slideStatus: any;
|
||||
}): PipelineTraceSummary {
|
||||
const slideData = args.slideStatus?.data ?? {};
|
||||
const rendered = new Set(arrayOfStrings(slideData.content_rendered_section_ids));
|
||||
const blocked = new Set(arrayOfStrings(slideData.render_blocked_section_ids));
|
||||
const filtered = new Set(arrayOfStrings(slideData.filtered_section_ids));
|
||||
const covered = new Set(arrayOfStrings(slideData.covered_section_ids));
|
||||
const sectionReasons = new Map<string, string[]>();
|
||||
|
||||
(Array.isArray(slideData.filtered_section_reasons) ? slideData.filtered_section_reasons : [])
|
||||
.forEach((reason: any) => {
|
||||
arrayOfStrings(reason?.section_ids).forEach((id) => {
|
||||
pushReason(sectionReasons, id, formatFilteredReason(reason));
|
||||
});
|
||||
});
|
||||
|
||||
(Array.isArray(slideData.adapter_needed_units) ? slideData.adapter_needed_units : [])
|
||||
.forEach((unit: any) => {
|
||||
const reason = unit?.reason ?? "adapter_needed";
|
||||
arrayOfStrings(unit?.source_section_ids).forEach((id) => {
|
||||
pushReason(sectionReasons, id, String(reason));
|
||||
});
|
||||
});
|
||||
|
||||
filtered.forEach((id) => pushReason(sectionReasons, id, "filtered"));
|
||||
|
||||
const sections = (Array.isArray(args.normalized?.data?.sections) ? args.normalized.data.sections : [])
|
||||
.map((section: any, index: number): PipelineTraceSection => {
|
||||
const id = section.section_id ?? `section-${index + 1}`;
|
||||
const childIds = Array.isArray(section.sub_sections)
|
||||
? section.sub_sections
|
||||
.map((sub: any, subIndex: number) => sub.section_id ?? `${id}-sub-${subIndex + 1}`)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
let state: PipelineTraceSection["state"] = "uncovered";
|
||||
if (rendered.has(id)) state = "rendered";
|
||||
else if (blocked.has(id)) state = "render_blocked";
|
||||
else if (filtered.has(id)) state = "filtered";
|
||||
else if (covered.has(id)) state = "covered_not_rendered";
|
||||
return {
|
||||
id,
|
||||
title: section.title ?? "",
|
||||
child_ids: childIds,
|
||||
state,
|
||||
reasons: sectionReasons.get(id) ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
const compositionByUnit = new Map<string, any>();
|
||||
const selectedUnits = Array.isArray(args.compositionPlan?.data?.selected_units)
|
||||
? args.compositionPlan.data.selected_units
|
||||
: [];
|
||||
selectedUnits.forEach((unit: any) => {
|
||||
const unitId = arrayOfStrings(unit.source_section_ids).join("+");
|
||||
if (unitId) compositionByUnit.set(unitId, unit);
|
||||
});
|
||||
|
||||
const appUnits = Array.isArray(args.applicationPlan?.data?.units)
|
||||
? args.applicationPlan.data.units
|
||||
: [];
|
||||
const units = appUnits.map((unit: any): PipelineTraceUnit => {
|
||||
const sectionIds = typeof unit.unit_id === "string"
|
||||
? unit.unit_id.split("+").filter(Boolean)
|
||||
: arrayOfStrings(unit.source_section_ids);
|
||||
const compositionUnit = compositionByUnit.get(sectionIds.join("+"));
|
||||
const warnings: string[] = [];
|
||||
if (sectionIds.length > 1) warnings.push("merged_sections");
|
||||
const selectionPath = unit.selection_path ?? compositionUnit?.selection_path ?? null;
|
||||
if (typeof selectionPath === "string" && selectionPath.includes("fallback")) warnings.push(selectionPath);
|
||||
const candidateCount = buildFrameCandidatesForUnit(unit, compositionUnit).length;
|
||||
if (candidateCount === 0) warnings.push("no_frame_candidates");
|
||||
|
||||
return {
|
||||
unit_id: sectionIds.join("+") || unit.unit_id || "",
|
||||
source_section_ids: sectionIds,
|
||||
merge_type: unit.merge_type ?? compositionUnit?.merge_type ?? null,
|
||||
selected_frame:
|
||||
unit.current_default_candidate ??
|
||||
unit.template_id ??
|
||||
compositionUnit?.frame_template_id ??
|
||||
null,
|
||||
label: unit.label ?? compositionUnit?.label ?? null,
|
||||
candidate_count: candidateCount,
|
||||
selection_path: selectionPath,
|
||||
warnings,
|
||||
};
|
||||
});
|
||||
|
||||
const adapterByPosition = new Map<string, any>();
|
||||
(Array.isArray(slideData.adapter_needed_units) ? slideData.adapter_needed_units : [])
|
||||
.forEach((unit: any) => {
|
||||
if (unit.position) adapterByPosition.set(unit.position, unit);
|
||||
});
|
||||
|
||||
const zones = (Array.isArray(args.slotPayload?.data?.per_zone) ? args.slotPayload.data.per_zone : [])
|
||||
.map((zone: any, index: number): PipelineTraceZone => {
|
||||
const { status, keyCount } = slotPayloadStatus(zone);
|
||||
const adapterNeeded = adapterByPosition.get(zone.position);
|
||||
const appUnit = appUnits[index] ?? {};
|
||||
const sectionIds = arrayOfStrings(adapterNeeded?.source_section_ids).length > 0
|
||||
? arrayOfStrings(adapterNeeded.source_section_ids)
|
||||
: typeof appUnit.unit_id === "string"
|
||||
? appUnit.unit_id.split("+").filter(Boolean)
|
||||
: [];
|
||||
const warnings: string[] = [];
|
||||
if (zone.template_id === "__empty__") warnings.push("__empty__");
|
||||
if (status === "empty") warnings.push("empty_slot_payload");
|
||||
if (adapterNeeded) warnings.push(adapterNeeded.reason ?? "adapter_needed");
|
||||
return {
|
||||
position: zone.position ?? `zone-${index}`,
|
||||
source_section_ids: sectionIds,
|
||||
template_id: zone.template_id ?? null,
|
||||
slot_status: status,
|
||||
slot_key_count: keyCount,
|
||||
warnings,
|
||||
};
|
||||
});
|
||||
|
||||
const warnings = [
|
||||
...arrayOfStrings(slideData.filtered_section_ids).map((id) => `filtered:${id}`),
|
||||
...arrayOfStrings(slideData.render_blocked_section_ids).map((id) => `render_blocked:${id}`),
|
||||
];
|
||||
if ((slideData.adapter_needed_count ?? 0) > 0) {
|
||||
warnings.push(`adapter_needed:${slideData.adapter_needed_count}`);
|
||||
}
|
||||
|
||||
return {
|
||||
sections,
|
||||
units,
|
||||
zones,
|
||||
status: slideData.overall ?? "UNKNOWN",
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAiTraceSummary(
|
||||
aiRepairArtifact: any | null,
|
||||
aiRepairStatus: AiRepairStatus | null | undefined,
|
||||
): AiTraceSummary {
|
||||
if (!aiRepairArtifact?.data) {
|
||||
return {
|
||||
artifact_present: false,
|
||||
ai_enabled: null,
|
||||
status: aiRepairStatus?.status ?? "missing_artifact",
|
||||
ai_called_count: 0,
|
||||
eligible_count: 0,
|
||||
skipped_count: 0,
|
||||
error_count: 0,
|
||||
skip_reasons: {},
|
||||
provider: null,
|
||||
model: null,
|
||||
coverage_status: aiRepairStatus?.coverage_status ?? null,
|
||||
human_review_required: aiRepairStatus?.human_review_required ?? false,
|
||||
units: [],
|
||||
warnings: ["step12_ai_repair.json missing"],
|
||||
};
|
||||
}
|
||||
|
||||
const rawUnits = Array.isArray(aiRepairArtifact.data.per_unit)
|
||||
? aiRepairArtifact.data.per_unit
|
||||
: [];
|
||||
const units: AiTraceUnit[] = rawUnits.map((unit: any) => ({
|
||||
unit_index: typeof unit.unit_index === "number" ? unit.unit_index : null,
|
||||
source_section_ids: arrayOfStrings(unit.source_section_ids),
|
||||
frame_template_id: unit.frame_template_id ?? null,
|
||||
route_hint: unit.route_hint ?? null,
|
||||
provisional: unit.provisional === true,
|
||||
ai_called: unit.ai_called === true,
|
||||
skip_reason: unit.skip_reason ?? null,
|
||||
apply_status: unit.apply_status ?? null,
|
||||
api_error_kind: unit.api_error_kind ?? null,
|
||||
error: unit.error ?? null,
|
||||
}));
|
||||
|
||||
const skipReasons: Record<string, number> = {};
|
||||
units.forEach((unit) => {
|
||||
const key = unit.skip_reason ?? (unit.ai_called ? "called" : "unknown");
|
||||
skipReasons[key] = (skipReasons[key] ?? 0) + 1;
|
||||
});
|
||||
const warnings: string[] = [];
|
||||
units
|
||||
.filter((unit) => unit.error)
|
||||
.forEach((unit) => warnings.push(`error:${unit.unit_index ?? "?"}`));
|
||||
units
|
||||
.filter((unit) => unit.api_error_kind)
|
||||
.forEach((unit) => warnings.push(`api_${unit.api_error_kind}:${unit.unit_index ?? "?"}`));
|
||||
if (units.some((unit) => unit.skip_reason === "router_short_circuit")) {
|
||||
warnings.push("router_short_circuit");
|
||||
}
|
||||
const eligibleUnits = units.filter(
|
||||
(unit) => unit.provisional && unit.route_hint === "ai_adaptation_required",
|
||||
);
|
||||
const aiEnabled =
|
||||
units.some((unit) => unit.ai_called)
|
||||
? true
|
||||
: eligibleUnits.length > 0 && eligibleUnits.every((unit) => unit.skip_reason === "router_short_circuit")
|
||||
? false
|
||||
: null;
|
||||
|
||||
return {
|
||||
artifact_present: true,
|
||||
ai_enabled: aiEnabled,
|
||||
status: aiRepairStatus?.status ?? aiRepairArtifact.step_status ?? "unknown",
|
||||
ai_called_count: units.filter((unit) => unit.ai_called).length,
|
||||
eligible_count: units.filter((unit) => unit.provisional && unit.route_hint === "ai_adaptation_required").length,
|
||||
skipped_count: units.filter((unit) => !unit.ai_called).length,
|
||||
error_count: units.filter((unit) => !!unit.error).length,
|
||||
skip_reasons: skipReasons,
|
||||
provider: "anthropic",
|
||||
model: null,
|
||||
coverage_status:
|
||||
aiRepairStatus?.coverage_status ??
|
||||
aiRepairArtifact.data.coverage_invariant?.status ??
|
||||
null,
|
||||
human_review_required: aiRepairStatus?.human_review_required ?? false,
|
||||
units,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
console.log(`[Phase Z] loadRun: ${runId}`);
|
||||
const base = `/data/runs/${runId}`;
|
||||
@@ -456,6 +931,14 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
const fetchJsonOptional = async (path: string) => {
|
||||
const res = await fetch(`${base}/${path}`);
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch ${path}: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
const fetchText = async (path: string) => {
|
||||
const res = await fetch(`${base}/${path}`);
|
||||
if (!res.ok) {
|
||||
@@ -468,21 +951,28 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
mdxSource,
|
||||
upload,
|
||||
normalized,
|
||||
compositionPlan,
|
||||
layout,
|
||||
zoneRegion,
|
||||
applicationPlan,
|
||||
slotPayload,
|
||||
slideStatus,
|
||||
aiRepair,
|
||||
] = await Promise.all([
|
||||
fetchText("steps/step01_mdx_source.md"),
|
||||
fetchJson("steps/step01_mdx_upload.json"),
|
||||
fetchJson("steps/step02_normalized.json"),
|
||||
fetchJson("steps/step06_composition_plan.json"),
|
||||
fetchJson("steps/step07_layout.json"),
|
||||
fetchJson("steps/step08_zone_region_ratios.json"),
|
||||
fetchJson("steps/step09_application_plan.json"),
|
||||
fetchJson("steps/step12_slot_payload.json"),
|
||||
fetchJson("steps/step20_slide_status.json"),
|
||||
fetchJsonOptional("steps/step12_ai_repair.json"),
|
||||
]);
|
||||
|
||||
// ── RunMeta ──
|
||||
const aiRepairStatus = (slideStatus.data?.ai_repair_status ?? null) as AiRepairStatus | null;
|
||||
const runMeta: RunMeta = {
|
||||
run_id: upload.data?.run_id ?? runId,
|
||||
mdx_path: upload.data?.mdx_path ?? "",
|
||||
@@ -507,7 +997,15 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
z.display_strategy_candidates ?? [],
|
||||
])
|
||||
),
|
||||
ai_repair_status: (slideStatus.data?.ai_repair_status ?? null) as AiRepairStatus | null,
|
||||
ai_repair_status: aiRepairStatus,
|
||||
pipeline_trace: buildPipelineTraceSummary({
|
||||
normalized,
|
||||
compositionPlan,
|
||||
applicationPlan,
|
||||
slotPayload,
|
||||
slideStatus,
|
||||
}),
|
||||
ai_trace: buildAiTraceSummary(aiRepair, aiRepairStatus),
|
||||
};
|
||||
|
||||
// ── NormalizedContent ──
|
||||
@@ -562,6 +1060,16 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
const layoutPreset = (layout.data?.layout_preset ?? "single") as LayoutPresetId;
|
||||
const positions = computeZonePositions(layoutPreset);
|
||||
const units: any[] = applicationPlan.data?.units ?? [];
|
||||
const compositionUnitsById = new Map<string, any>();
|
||||
const selectedUnits = compositionPlan.data?.selected_units;
|
||||
if (Array.isArray(selectedUnits)) {
|
||||
selectedUnits.forEach((unit: any) => {
|
||||
const unitId = Array.isArray(unit.source_section_ids)
|
||||
? unit.source_section_ids.join("+")
|
||||
: "";
|
||||
if (unitId) compositionUnitsById.set(unitId, unit);
|
||||
});
|
||||
}
|
||||
|
||||
const zones = units.map((unit: any, idx: number) => {
|
||||
const posEntry = positions[idx] ?? {
|
||||
@@ -631,7 +1139,14 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
|
||||
let v4Source: any[];
|
||||
if (backendPolicyPayloadPresent) {
|
||||
// Emergency fix (2026-05-26) — sorted_candidate_evidence 는 primary (rank order)
|
||||
// 로 먼저 push (first-wins dedup map 이 순서 보존). 그 후 v4_all_judgments +
|
||||
// v4_candidates 도 push 해서 후보 pool 채움 (reject 포함, max TOP_N_FRAMES).
|
||||
// 이전 버그: sorted_candidate_evidence 만 사용 → "선택된 후보 1개" 만 frontend panel
|
||||
// 에 surface, 나머지 reject/light_edit/restructure 후보 사라짐.
|
||||
sortedCandidateEvidence!.forEach(pushCandidate);
|
||||
(unit.v4_all_judgments ?? []).forEach(pushCandidate);
|
||||
(unit.v4_candidates ?? []).forEach(pushCandidate);
|
||||
v4Source = Array.from(candidateMap.values());
|
||||
} else {
|
||||
// IMP-39 u4 — warn-fallback path. Legacy fixtures predating u3 (or
|
||||
@@ -698,6 +1213,8 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
frameId: c.frame_id,
|
||||
v4Label: c.v4_label,
|
||||
phaseZStatus: c.phase_z_status,
|
||||
coverageState: c.coverage_state ?? c.coverageState,
|
||||
candidateStatus: c.candidate_status ?? c.candidateStatus,
|
||||
filteredForDirectExecution: c.filtered_for_direct_execution,
|
||||
routeHint: c.route_hint,
|
||||
decision: c.decision,
|
||||
@@ -713,6 +1230,15 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
});
|
||||
});
|
||||
|
||||
const mergedFrameCandidates = buildFrameCandidatesForUnit(
|
||||
unit,
|
||||
compositionUnitsById.get(unit.unit_id),
|
||||
);
|
||||
const effectiveFrameCandidates =
|
||||
mergedFrameCandidates.length > 0 ? mergedFrameCandidates : frameCandidates;
|
||||
const defaultFrameId =
|
||||
unit.current_default_candidate ?? effectiveFrameCandidates[0]?.id ?? null;
|
||||
|
||||
const displayStrategy = (
|
||||
runMeta.display_strategy_candidates_by_zone[posEntry.name]?.[0] ??
|
||||
"inline_full"
|
||||
@@ -737,10 +1263,10 @@ export async function loadRun(runId: string): Promise<LoadRunResult> {
|
||||
content_unit_ids: [],
|
||||
frame_match_strategy: {
|
||||
kind: "frame_match" as const,
|
||||
frame_id: unit.current_default_candidate ?? null,
|
||||
frame_id: defaultFrameId,
|
||||
display_strategy: displayStrategy,
|
||||
},
|
||||
frame_candidates: frameCandidates,
|
||||
frame_candidates: effectiveFrameCandidates,
|
||||
},
|
||||
],
|
||||
region_layout_type: regionId,
|
||||
|
||||
@@ -162,6 +162,10 @@ export interface FrameCandidate {
|
||||
v4Label?: 'use_as_is' | 'light_edit' | 'restructure' | 'reject';
|
||||
/** Phase Z status enum (e.g. "auto_renderable", "fallback_candidate"). Open vocabulary. */
|
||||
phaseZStatus?: string;
|
||||
/** T21.6 coverage-state contract for candidate UX. */
|
||||
coverageState?: 'covered_native' | 'covered_via_expand' | 'requires_adaptation' | 'unsupported' | string;
|
||||
/** Backend 3-status pool classification (auto/adaptation/blocked). */
|
||||
candidateStatus?: string;
|
||||
/** True when status is outside MVP1_ALLOWED_STATUSES (= excluded from direct render path). */
|
||||
filteredForDirectExecution?: boolean;
|
||||
/** Execution route mapped from `label` (direct_render / deterministic_minor_adjustment /
|
||||
|
||||
@@ -8,20 +8,20 @@ import type {
|
||||
} from "../services/userOverridesApi";
|
||||
import { computeZonePositions } from "../services/designAgentApi";
|
||||
|
||||
// ─── IMP-52 u6 — restore-on-reopen helpers (pure, exported for testing) ────
|
||||
// ??? IMP-52 u6 ??restore-on-reopen helpers (pure, exported for testing) ????
|
||||
// These helpers compose persisted `user_overrides.json` payloads (typed by
|
||||
// the u5 service) onto the in-memory `UserSelection`. They live here rather
|
||||
// than inline in Home.tsx so vitest can drive them in a node environment
|
||||
// without booting React or pulling in the radix-ui / lucide UI deps that
|
||||
// Home.tsx requires. Home.tsx wires these into:
|
||||
// • handleFileUpload (pre-Generate layout / zone_geometries / zone_sections
|
||||
// ??handleFileUpload (pre-Generate layout / zone_geometries / zone_sections
|
||||
// seed so handleGenerate's CLI-args build picks them up)
|
||||
// • handleGenerate post-loadRun (frame remap unit_id → region.id over the
|
||||
// ??handleGenerate post-loadRun (frame remap unit_id ??region.id over the
|
||||
// freshly built slidePlan)
|
||||
// The on-disk schema and clear-sentinel semantics are owned by:
|
||||
// • src/user_overrides_io.py (KNOWN_AXES, u1)
|
||||
// • Front/vite.config.ts mergeUserOverrides (u4)
|
||||
// • Front/client/src/services/userOverridesApi.ts (UserOverrides type, u5)
|
||||
// ??src/user_overrides_io.py (KNOWN_AXES, u1)
|
||||
// ??Front/vite.config.ts mergeUserOverrides (u4)
|
||||
// ??Front/client/src/services/userOverridesApi.ts (UserOverrides type, u5)
|
||||
// Any KNOWN_AXES drift must land in those files first.
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,101 @@ import { computeZonePositions } from "../services/designAgentApi";
|
||||
* so the same persisted file is read from both ends without translation.
|
||||
*/
|
||||
export function deriveUserOverridesKey(filename: string): string {
|
||||
return filename.replace(/\.mdx$/i, "");
|
||||
if (filename === "") return "";
|
||||
const basename = filename.split(/[\\/]/).pop() ?? filename;
|
||||
const noExt = basename.replace(/\.mdx$/i, "");
|
||||
return noExt.replace(/[^A-Za-z0-9_.-]/g, "_");
|
||||
}
|
||||
|
||||
export interface SubmittedPipelineOverridesForRestore {
|
||||
layout?: string;
|
||||
frames?: Record<string, string>;
|
||||
zoneGeometries?: Record<string, ZoneGeometryValue>;
|
||||
zoneSections?: Record<string, string[]>;
|
||||
}
|
||||
|
||||
function _copyStringMap(raw: unknown): Record<string, string> | undefined {
|
||||
if (!_isPlainObject(raw)) return undefined;
|
||||
const out: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(raw)) {
|
||||
if (k && typeof v === "string" && v.length > 0) out[k] = v;
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
function _copyZoneSections(raw: unknown): Record<string, string[]> | undefined {
|
||||
if (!_isPlainObject(raw)) return undefined;
|
||||
const out: Record<string, string[]> = {};
|
||||
for (const [zoneId, sids] of Object.entries(raw)) {
|
||||
if (!zoneId || !Array.isArray(sids)) continue;
|
||||
const cleaned = sids.filter((sid) => typeof sid === "string" && sid.trim());
|
||||
if (cleaned.length > 0) out[zoneId] = cleaned;
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
function _copyZoneGeometries(raw: unknown): Record<string, ZoneGeometryValue> | undefined {
|
||||
if (!_isPlainObject(raw)) return undefined;
|
||||
const out: Record<string, ZoneGeometryValue> = {};
|
||||
for (const [zoneId, geom] of Object.entries(raw)) {
|
||||
if (!zoneId || !_isPlainObject(geom)) continue;
|
||||
const { x, y, w, h } = geom;
|
||||
if (
|
||||
typeof x === "number" &&
|
||||
typeof y === "number" &&
|
||||
typeof w === "number" &&
|
||||
typeof h === "number"
|
||||
) {
|
||||
out[zoneId] = { x, y, w, h };
|
||||
}
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the post-run restore document from the latest persisted payload plus
|
||||
* the exact override payload submitted to `/api/run`.
|
||||
*
|
||||
* The generated HTML is driven by the submitted override payload, so the
|
||||
* frontend state rebuilt after `loadRun()` must prefer that same payload over
|
||||
* any stale layout/frame/zone axes that were read at file-upload time. Other
|
||||
* persisted axes (text/image/structure edits) are preserved.
|
||||
*/
|
||||
export function mergeSubmittedPipelineOverridesForRestore(
|
||||
persisted: Partial<UserOverrides> | null | undefined,
|
||||
submitted: SubmittedPipelineOverridesForRestore | null | undefined,
|
||||
manualSectionAssignment: boolean,
|
||||
): Partial<UserOverrides> {
|
||||
const next: Partial<UserOverrides> = { ...(persisted ?? {}) };
|
||||
|
||||
// These axes are owned by the current Generate payload. Clear stale values
|
||||
// first so an older user_overrides file cannot reappear in the UI after a
|
||||
// successful render with a different explicit override.
|
||||
delete next.layout;
|
||||
delete next.frames;
|
||||
delete next.zone_geometries;
|
||||
delete next.zone_sections;
|
||||
next.manual_section_assignment = false;
|
||||
|
||||
if (!submitted || typeof submitted !== "object") return next;
|
||||
|
||||
if (typeof submitted.layout === "string" && submitted.layout.length > 0) {
|
||||
next.layout = submitted.layout;
|
||||
}
|
||||
|
||||
const frames = _copyStringMap(submitted.frames);
|
||||
if (frames) next.frames = frames;
|
||||
|
||||
const zoneGeometries = _copyZoneGeometries(submitted.zoneGeometries);
|
||||
if (zoneGeometries) next.zone_geometries = zoneGeometries;
|
||||
|
||||
if (manualSectionAssignment === true) {
|
||||
const zoneSections = _copyZoneSections(submitted.zoneSections);
|
||||
next.zone_sections = zoneSections ?? {};
|
||||
next.manual_section_assignment = true;
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
const LAYOUT_PRESET_IDS = new Set<string>([
|
||||
@@ -48,7 +142,7 @@ const LAYOUT_PRESET_IDS = new Set<string>([
|
||||
/**
|
||||
* Layer the three non-frame axes from a persisted `user_overrides.json`
|
||||
* payload onto an existing `UserSelection`. Foreign / unrecognized payload
|
||||
* shapes are silently ignored — the u5 GET path already returns `{}` on
|
||||
* shapes are silently ignored ??the u5 GET path already returns `{}` on
|
||||
* corrupt files, but we revalidate here so hand-edited files or future
|
||||
* forward-compat axes cannot poison the in-memory state.
|
||||
*
|
||||
@@ -79,11 +173,11 @@ export function applyPersistedNonFrameOverrides(
|
||||
) {
|
||||
next.zone_sections = { ...persisted.zone_sections };
|
||||
}
|
||||
// IMP-51 (#79) u11 — layer the 5th persisted axis (`image_overrides`) by
|
||||
// IMP-51 (#79) u11 ??layer the 5th persisted axis (`image_overrides`) by
|
||||
// the same array / non-object guard the zone_geometries branch uses. The
|
||||
// u3 typed client (services/userOverridesApi.ts) shape and the on-disk
|
||||
// KNOWN_AXES entry (src/user_overrides_io.py u1) are both flat dicts
|
||||
// (image_id → {x,y,w,h} percent-of-slide), so a shallow copy is enough.
|
||||
// (image_id ??{x,y,w,h} percent-of-slide), so a shallow copy is enough.
|
||||
if (
|
||||
persisted.image_overrides &&
|
||||
typeof persisted.image_overrides === "object" &&
|
||||
@@ -91,7 +185,7 @@ export function applyPersistedNonFrameOverrides(
|
||||
) {
|
||||
next.image_overrides = { ...persisted.image_overrides };
|
||||
}
|
||||
// IMP-55 (#93) u3 — restore the bool intent marker only when the persisted
|
||||
// IMP-55 (#93) u3 ??restore the bool intent marker only when the persisted
|
||||
// value is a real `boolean`. A missing axis, `null` (the u4 clear sentinel
|
||||
// observed post-flush), or any non-boolean shape (string "true", 1, {})
|
||||
// intentionally falls through to the `createInitialUserSelection` seed of
|
||||
@@ -101,11 +195,11 @@ export function applyPersistedNonFrameOverrides(
|
||||
// `true` MUST end up as `false` in memory to avoid resurrecting stale
|
||||
// auto-carry assignments as user intent. Both `true` and `false` are
|
||||
// restored verbatim (the explicit `false` from u12's apply/cancel write
|
||||
// is meaningful — it pins the marker off across reopens).
|
||||
// is meaningful ??it pins the marker off across reopens).
|
||||
if (typeof persisted.manual_section_assignment === "boolean") {
|
||||
next.manual_section_assignment = persisted.manual_section_assignment;
|
||||
}
|
||||
// IMP-56 (#90) u15 — layer the two Step-22 persist axes through the
|
||||
// IMP-56 (#90) u15 ??layer the two Step-22 persist axes through the
|
||||
// u10 extract helpers; their `_isPlainObject` + dedupe gates already
|
||||
// sanitize foreign / hand-edited payloads, so reopen never poisons
|
||||
// memory with non-string values or non-list slot_order entries.
|
||||
@@ -114,7 +208,7 @@ export function applyPersistedNonFrameOverrides(
|
||||
return { ...selection, overrides: next };
|
||||
}
|
||||
|
||||
// ─── IMP-56 #90 u10 — typed extract helpers for the two new persist axes ───
|
||||
// ??? IMP-56 #90 u10 ??typed extract helpers for the two new persist axes ???
|
||||
// Pure helpers that defensively sanitize Step-22 text_overrides and
|
||||
// structure_overrides payloads off a `Partial<UserOverrides>` (typed by u10's
|
||||
// userOverridesApi extension). They mirror the backend validation gates
|
||||
@@ -174,8 +268,8 @@ export function extractPersistedStructureOverrides(
|
||||
}
|
||||
|
||||
/**
|
||||
* Remap persisted frames (`unit_id` → template_id) to the in-memory
|
||||
* `zone_frames` (region.id → template_id) using the freshly built
|
||||
* Remap persisted frames (`unit_id` ??template_id) to the in-memory
|
||||
* `zone_frames` (region.id ??template_id) using the freshly built
|
||||
* slidePlan zones. `unit_id` follows handleGenerate's convention:
|
||||
* `zone.section_ids.join("+")`. Persisted entries whose unit_id no longer
|
||||
* matches any zone (e.g. user changed zone_sections between sessions) are
|
||||
@@ -203,8 +297,8 @@ export function remapPersistedFramesToZoneFrames(
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase Z 초기 선택 상태 생성
|
||||
* SlidePlan의 결과를 초기 값으로 사용 (Step 11까지의 결과 반영)
|
||||
* Phase Z 珥덇린 ?좏깮 ?곹깭 ?앹꽦
|
||||
* SlidePlan??寃곌낵瑜?珥덇린 媛믪쑝濡??ъ슜 (Step 11源뚯???寃곌낵 諛섏쁺)
|
||||
*/
|
||||
export function createInitialUserSelection(slidePlan?: SlidePlan | null): UserSelection {
|
||||
const initialSections: Record<string, string[]> = {};
|
||||
@@ -212,16 +306,16 @@ export function createInitialUserSelection(slidePlan?: SlidePlan | null): UserSe
|
||||
|
||||
if (slidePlan) {
|
||||
slidePlan.zones.forEach(zone => {
|
||||
// 1. 모든 섹션을 각자의 지정된 존에 할당 (초안 배치)
|
||||
// 1. 紐⑤뱺 ?뱀뀡??媛곸옄??吏?뺣맂 議댁뿉 ?좊떦 (珥덉븞 諛곗튂)
|
||||
initialSections[zone.zone_id] = [...zone.section_ids];
|
||||
|
||||
// 2. 각 리전의 기본 frame.
|
||||
// 2026-05-14 — backend frame_match_strategy.frame_id 가 있을 때만 init.
|
||||
// null 인 경우 (backend current_default_candidate=None 등) frame_candidates[0]
|
||||
// 로 자동 채우지 않음 → SlideCanvas 의 preview overlay 트리거 조건
|
||||
// (override !== default) 안 발동. 사용자가 직접 frame 클릭해야 preview 보임.
|
||||
// 배경 : 04-1 같은 case 에서 backend selection_path=rank_1 (env toggle 통과)
|
||||
// 이어도 current_default=None 이면 default 와 override mismatch 로 preview 강제 발동.
|
||||
// 2. 媛?由ъ쟾??湲곕낯 frame.
|
||||
// 2026-05-14 ??backend frame_match_strategy.frame_id 媛 ?덉쓣 ?뚮쭔 init.
|
||||
// null ??寃쎌슦 (backend current_default_candidate=None ?? frame_candidates[0]
|
||||
// 濡??먮룞 梨꾩슦吏 ?딆쓬 ??SlideCanvas ??preview overlay ?몃━嫄?議곌굔
|
||||
// (override !== default) ??諛쒕룞. ?ъ슜?먭? 吏곸젒 frame ?대┃?댁빞 preview 蹂댁엫.
|
||||
// 諛곌꼍 : 04-1 媛숈? case ?먯꽌 backend selection_path=rank_1 (env toggle ?듦낵)
|
||||
// ?댁뼱??current_default=None ?대㈃ default ? override mismatch 濡?preview 媛뺤젣 諛쒕룞.
|
||||
zone.internal_regions.forEach(region => {
|
||||
const topFrameId = region.frame_match_strategy.frame_id;
|
||||
if (topFrameId) {
|
||||
@@ -241,23 +335,23 @@ export function createInitialUserSelection(slidePlan?: SlidePlan | null): UserSe
|
||||
zone_sections: initialSections,
|
||||
zone_sizes: {},
|
||||
zone_geometries: {},
|
||||
// IMP-51 (#79) u11 — image_overrides axis starts empty; entries land
|
||||
// IMP-51 (#79) u11 ??image_overrides axis starts empty; entries land
|
||||
// here via `saveImageOverride` (SlideCanvas drag/resize handler) and
|
||||
// are seeded on reopen via `applyPersistedNonFrameOverrides`.
|
||||
image_overrides: {},
|
||||
// IMP-56 (#90) u15 — Step-22 axes seeded empty. Entries land here
|
||||
// IMP-56 (#90) u15 ??Step-22 axes seeded empty. Entries land here
|
||||
// via `saveTextOverride` (u13 focusout capture) and
|
||||
// `saveStructureOverride` (u14 overlay) and are restored on reopen
|
||||
// via `applyPersistedNonFrameOverrides`.
|
||||
text_overrides: {},
|
||||
structure_overrides: {},
|
||||
// IMP-55 (#93) u3 — bool intent marker seeded `false` so a fresh
|
||||
// IMP-55 (#93) u3 ??bool intent marker seeded `false` so a fresh
|
||||
// MDX open (no persisted file, or persisted file with axis absent)
|
||||
// never forwards `overrides.zoneSections` to the backend. The marker
|
||||
// flips to `true` only via the real drag-drop path (Home.tsx u6) and
|
||||
// is reset to `false` by layout apply/cancel auto-carry (u5/u12).
|
||||
// `applyPersistedNonFrameOverrides` may restore a persisted boolean
|
||||
// verbatim on reopen — see the bool-only guard there.
|
||||
// verbatim on reopen ??see the bool-only guard there.
|
||||
manual_section_assignment: false,
|
||||
},
|
||||
};
|
||||
@@ -281,12 +375,12 @@ export function saveZoneGeometry(
|
||||
}
|
||||
|
||||
/**
|
||||
* IMP-51 (#79) u11 — record a single `image_id` → slide-absolute percent
|
||||
* IMP-51 (#79) u11 ??record a single `image_id` ??slide-absolute percent
|
||||
* geometry on the in-memory selection. Mirrors `saveZoneGeometry` but on
|
||||
* the 5th persisted axis (`image_overrides`); the SlideCanvas drag/resize
|
||||
* handler (u8) emits one entry per pointer move, and u10's Home wiring
|
||||
* funnels each emit through this helper before scheduling the debounced
|
||||
* PUT. Pure / immutable — returns a fresh `UserSelection`; the input is
|
||||
* PUT. Pure / immutable ??returns a fresh `UserSelection`; the input is
|
||||
* never mutated. Existing entries for the same `imageId` are replaced.
|
||||
*/
|
||||
export function saveImageOverride(
|
||||
@@ -307,7 +401,7 @@ export function saveImageOverride(
|
||||
}
|
||||
|
||||
/**
|
||||
* IMP-56 (#90) u15 — record a single text-line capture (zone_id, text_path,
|
||||
* IMP-56 (#90) u15 ??record a single text-line capture (zone_id, text_path,
|
||||
* value) onto the in-memory selection's `text_overrides` axis. Mirrors
|
||||
* `saveImageOverride` (pure / immutable). u13's focusout capture emits one
|
||||
* entry per finished edit; Home u15's handler funnels each emit through this
|
||||
@@ -333,7 +427,7 @@ export function saveTextOverride(
|
||||
}
|
||||
|
||||
/**
|
||||
* IMP-56 (#90) u15 — record a single structure capture (zone_id ↦
|
||||
* IMP-56 (#90) u15 ??record a single structure capture (zone_id ??
|
||||
* {slot_order, hidden_slots}) onto the in-memory selection's
|
||||
* `structure_overrides` axis. Scope-locked to slot reorder + hide (frame
|
||||
* swap stays on the `frames` axis). u14's overlay emits one entry per
|
||||
@@ -374,7 +468,7 @@ export function saveZoneSizes(selection: UserSelection, groupId: string, sizes:
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 섹션을 새로운 존으로 이동 (Drag & Drop)
|
||||
* ?뱀젙 ?뱀뀡???덈줈??議댁쑝濡??대룞 (Drag & Drop)
|
||||
*/
|
||||
export function moveSectionToZone(
|
||||
selection: UserSelection,
|
||||
@@ -383,12 +477,12 @@ export function moveSectionToZone(
|
||||
): UserSelection {
|
||||
const newZoneSections = { ...selection.overrides.zone_sections };
|
||||
|
||||
// 1. 모든 존에서 해당 섹션 제거 (이동 전 위치 클리어)
|
||||
// 1. 紐⑤뱺 議댁뿉???대떦 ?뱀뀡 ?쒓굅 (?대룞 ???꾩튂 ?대━??
|
||||
Object.keys(newZoneSections).forEach(zid => {
|
||||
newZoneSections[zid] = newZoneSections[zid].filter(id => id !== sectionId);
|
||||
});
|
||||
|
||||
// 2. 타겟 존에 섹션 추가
|
||||
// 2. ?寃?議댁뿉 ?뱀뀡 異붽?
|
||||
if (!newZoneSections[targetZoneId]) {
|
||||
newZoneSections[targetZoneId] = [];
|
||||
}
|
||||
@@ -409,7 +503,7 @@ export function selectZone(selection: UserSelection, zoneId: string | null): Use
|
||||
return {
|
||||
...selection,
|
||||
selectedZoneId: zoneId,
|
||||
selectedRegionId: null, // Zone이 바뀌면 Region 선택 해제
|
||||
selectedRegionId: null, // Zone??諛붾뚮㈃ Region ?좏깮 ?댁젣
|
||||
};
|
||||
}
|
||||
|
||||
@@ -444,16 +538,16 @@ export function applyFrame(selection: UserSelection, regionId: string, frameId:
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택된 Zone 객체 반환
|
||||
* ?꾩옱 ?좏깮??Zone 媛앹껜 諛섑솚
|
||||
*/
|
||||
export function getSelectedZone(slidePlan: SlidePlan | null, selection: UserSelection): Zone | null {
|
||||
if (!slidePlan || !selection.selectedZoneId) return null;
|
||||
// id 또는 zone_id 매칭
|
||||
// id ?먮뒗 zone_id 留ㅼ묶
|
||||
return slidePlan.zones.find(z => z.id === selection.selectedZoneId || z.zone_id === selection.selectedZoneId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 선택된 Region 객체 반환
|
||||
* ?꾩옱 ?좏깮??Region 媛앹껜 諛섑솚
|
||||
*/
|
||||
export function getSelectedRegion(zone: Zone | null, selection: UserSelection): InternalRegion | null {
|
||||
if (!zone || !selection.selectedRegionId) return null;
|
||||
@@ -461,21 +555,21 @@ export function getSelectedRegion(zone: Zone | null, selection: UserSelection):
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 Zone에 할당된 섹션 ID 목록 반환 (오버라이드 우선)
|
||||
* ?뱀젙 Zone???좊떦???뱀뀡 ID 紐⑸줉 諛섑솚 (?ㅻ쾭?쇱씠???곗꽑)
|
||||
*/
|
||||
export function getSectionsForZone(zone: Zone, selection: UserSelection): string[] {
|
||||
return selection.overrides.zone_sections[zone.zone_id] || zone.section_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 최종 유효 레이아웃 ID 반환
|
||||
* 理쒖쥌 ?좏슚 ?덉씠?꾩썐 ID 諛섑솚
|
||||
*/
|
||||
export function getEffectiveLayoutId(slidePlan: SlidePlan | null, selection: UserSelection): LayoutPresetId {
|
||||
if (selection.overrides.layout_preset) return selection.overrides.layout_preset;
|
||||
return slidePlan?.layout_preset || 'single';
|
||||
}
|
||||
|
||||
// ─── IMP-44 (#73) u3 — zone_geometries layout-mismatch validation ───────────
|
||||
// ??? IMP-44 (#73) u3 ??zone_geometries layout-mismatch validation ???????????
|
||||
// Pure helper paired with the backend [override-warning] guards added in u1
|
||||
// (1-D horizontal-2 / vertical-2 branches of `build_layout_css`) and u2 (2-D
|
||||
// `_override_to_grid_tracks` call site). Same WARN+DROP / KEEP-known contract,
|
||||
@@ -484,7 +578,7 @@ export function getEffectiveLayoutId(slidePlan: SlidePlan | null, selection: Use
|
||||
//
|
||||
// Source of truth for expected positions = `computeZonePositions(layoutPreset)`
|
||||
// (designAgentApi.ts), which mirrors backend `layouts.yaml` (positions field).
|
||||
// Unknown layout (null / undefined / not in LAYOUT_PRESET_IDS) ⇒ fail-safe
|
||||
// Unknown layout (null / undefined / not in LAYOUT_PRESET_IDS) ??fail-safe
|
||||
// drop-all: caller has no contract for projecting geometries onto an unknown
|
||||
// preset, so we keep zero keys rather than passing them through verbatim.
|
||||
|
||||
@@ -511,7 +605,7 @@ export function validateZoneGeometriesAgainstLayout(
|
||||
const safeGeoms =
|
||||
geoms && typeof geoms === "object" && !Array.isArray(geoms) ? geoms : null;
|
||||
|
||||
// Unknown-layout fail-safe — drop everything; no expected positions known.
|
||||
// Unknown-layout fail-safe ??drop everything; no expected positions known.
|
||||
if (typeof layoutPreset !== "string" || !LAYOUT_PRESET_IDS.has(layoutPreset)) {
|
||||
if (safeGeoms) {
|
||||
for (const [k, v] of Object.entries(safeGeoms)) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildFrameCandidatesForUnit } from "../src/services/designAgentApi";
|
||||
|
||||
describe("buildFrameCandidatesForUnit (#98 Task 2)", () => {
|
||||
it("merges sorted evidence, v4 candidates, all judgments, and app candidates up to six", () => {
|
||||
const unit = {
|
||||
unit_id: "03-1",
|
||||
current_default_candidate: "frame_a",
|
||||
sorted_candidate_evidence: [
|
||||
{ template_id: "frame_a", frame_number: 1, label: "use_as_is", confidence: 0.9, rank: 1, coverage_state: "covered_native", candidate_status: "auto_renderable" },
|
||||
],
|
||||
v4_candidates: [
|
||||
{ template_id: "frame_b", frame_number: 2, label: "light_edit", confidence: 0.8 },
|
||||
],
|
||||
v4_all_judgments: [
|
||||
{ template_id: "frame_c", frame_number: 3, label: "reject", confidence: 0.7, coverage_state: "requires_adaptation", candidate_status: "ai_adaptation_required" },
|
||||
{ template_id: "frame_d", frame_number: 4, label: "reject", confidence: 0.6 },
|
||||
{ template_id: "frame_e", frame_number: 5, label: "reject", confidence: 0.5 },
|
||||
{ template_id: "frame_f", frame_number: 6, label: "reject", confidence: 0.4 },
|
||||
{ template_id: "frame_g", frame_number: 7, label: "reject", confidence: 0.3 },
|
||||
],
|
||||
application_candidates: [
|
||||
{ template_id: "frame_b", application_mode: "same_frame_with_adjustment", auto_applicable: true },
|
||||
],
|
||||
};
|
||||
|
||||
const candidates = buildFrameCandidatesForUnit(unit);
|
||||
expect(candidates.map((c) => c.id)).toEqual([
|
||||
"frame_a",
|
||||
"frame_b",
|
||||
"frame_c",
|
||||
"frame_d",
|
||||
"frame_e",
|
||||
"frame_f",
|
||||
]);
|
||||
expect(candidates).toHaveLength(6);
|
||||
expect(candidates[1].applicationMode).toBe("same_frame_with_adjustment");
|
||||
expect(candidates[0].coverageState).toBe("covered_native");
|
||||
expect(candidates[2].coverageState).toBe("requires_adaptation");
|
||||
expect(candidates[2].candidateStatus).toBe("ai_adaptation_required");
|
||||
});
|
||||
|
||||
it("surfaces generic fallback from composition when Step 9 has no V4 sources", () => {
|
||||
const unit = {
|
||||
unit_id: "05-1+05-2",
|
||||
current_default_candidate: null,
|
||||
sorted_candidate_evidence: [],
|
||||
candidate_evidence: [],
|
||||
v4_candidates: [],
|
||||
v4_all_judgments: [],
|
||||
application_candidates: [],
|
||||
};
|
||||
const compositionUnit = {
|
||||
source_section_ids: ["05-1", "05-2"],
|
||||
frame_template_id: "three_parallel_requirements",
|
||||
frame_id: "1171281190",
|
||||
label: "reject",
|
||||
score: 0,
|
||||
phase_z_status: "fallback_candidate",
|
||||
selection_path: "generic_fallback",
|
||||
rationale: { ai_adaptation_required: true },
|
||||
};
|
||||
|
||||
const candidates = buildFrameCandidatesForUnit(unit, compositionUnit);
|
||||
expect(candidates).toHaveLength(1);
|
||||
expect(candidates[0]).toMatchObject({
|
||||
id: "three_parallel_requirements",
|
||||
label: "reject",
|
||||
coverageState: "requires_adaptation",
|
||||
routeHint: "ai_adaptation_required",
|
||||
decision: "selected",
|
||||
reason: "generic_fallback",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -64,7 +64,7 @@ describe("handleGenerate [DIAG raw overrides] (IMP-42 u4)", () => {
|
||||
it("places the DIAG console.log before the runPipeline call site", () => {
|
||||
const diagIdx = HANDLE_GENERATE_BODY.indexOf('console.log("[DIAG raw overrides]"');
|
||||
const runPipelineIdx = HANDLE_GENERATE_BODY.indexOf(
|
||||
"runPipeline(state.uploadedFile, overrides)",
|
||||
"runPipeline(",
|
||||
);
|
||||
expect(diagIdx).toBeGreaterThan(-1);
|
||||
expect(runPipelineIdx).toBeGreaterThan(-1);
|
||||
@@ -108,7 +108,7 @@ describe("handleGenerate [DIAG raw overrides] (IMP-42 u4)", () => {
|
||||
const flushIdx = HANDLE_GENERATE_BODY.indexOf("await flushUserOverrides()");
|
||||
const diagIdx = HANDLE_GENERATE_BODY.indexOf('console.log("[DIAG raw overrides]"');
|
||||
const runPipelineIdx = HANDLE_GENERATE_BODY.indexOf(
|
||||
"runPipeline(state.uploadedFile, overrides)",
|
||||
"runPipeline(",
|
||||
);
|
||||
expect(flushIdx).toBeGreaterThan(-1);
|
||||
expect(diagIdx).toBeGreaterThan(flushIdx);
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAiTraceSummary, buildPipelineTraceSummary } from "../src/services/designAgentApi";
|
||||
|
||||
describe("#98 Task 6 pipeline trace summary", () => {
|
||||
it("connects sections, units, zones, and render-blocked warnings", () => {
|
||||
const trace = buildPipelineTraceSummary({
|
||||
normalized: {
|
||||
data: {
|
||||
sections: [
|
||||
{ section_id: "01-1", title: "S1" },
|
||||
{ section_id: "01-2", title: "S2" },
|
||||
],
|
||||
},
|
||||
},
|
||||
compositionPlan: {
|
||||
data: {
|
||||
selected_units: [
|
||||
{
|
||||
source_section_ids: ["01-1"],
|
||||
frame_template_id: "frame_a",
|
||||
label: "use_as_is",
|
||||
selection_path: "rank_1",
|
||||
},
|
||||
{
|
||||
source_section_ids: ["01-2"],
|
||||
frame_template_id: "frame_b",
|
||||
label: "use_as_is",
|
||||
selection_path: "provisional_rank_1",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
applicationPlan: {
|
||||
data: {
|
||||
units: [
|
||||
{
|
||||
unit_id: "01-1",
|
||||
current_default_candidate: "frame_a",
|
||||
sorted_candidate_evidence: [
|
||||
{ template_id: "frame_a", label: "use_as_is", confidence: 0.9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
unit_id: "01-2",
|
||||
current_default_candidate: "frame_b",
|
||||
sorted_candidate_evidence: [
|
||||
{ template_id: "frame_b", label: "use_as_is", confidence: 0.8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
slotPayload: {
|
||||
data: {
|
||||
per_zone: [
|
||||
{ position: "top", template_id: "frame_a", slot_payload: { title: "S1" } },
|
||||
{ position: "bottom", template_id: "__empty__", slot_payload: {} },
|
||||
],
|
||||
},
|
||||
},
|
||||
slideStatus: {
|
||||
data: {
|
||||
overall: "PARTIAL_COVERAGE",
|
||||
covered_section_ids: ["01-1", "01-2"],
|
||||
content_rendered_section_ids: ["01-1"],
|
||||
filtered_section_ids: ["01-2"],
|
||||
render_blocked_section_ids: ["01-2"],
|
||||
adapter_needed_count: 1,
|
||||
adapter_needed_units: [
|
||||
{
|
||||
position: "bottom",
|
||||
source_section_ids: ["01-2"],
|
||||
reason: "fit_error",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(trace.sections.map((section) => [section.id, section.state])).toEqual([
|
||||
["01-1", "rendered"],
|
||||
["01-2", "render_blocked"],
|
||||
]);
|
||||
expect(trace.sections[1].reasons).toEqual([
|
||||
"fit_error",
|
||||
"filtered",
|
||||
]);
|
||||
expect(trace.units.map((unit) => [unit.unit_id, unit.candidate_count])).toEqual([
|
||||
["01-1", 1],
|
||||
["01-2", 1],
|
||||
]);
|
||||
expect(trace.zones[1]).toMatchObject({
|
||||
position: "bottom",
|
||||
source_section_ids: ["01-2"],
|
||||
template_id: "__empty__",
|
||||
slot_status: "empty",
|
||||
warnings: ["__empty__", "empty_slot_payload", "fit_error"],
|
||||
});
|
||||
expect(trace.warnings).toContain("render_blocked:01-2");
|
||||
expect(trace.warnings).toContain("adapter_needed:1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#98 Task 7 AI trace summary", () => {
|
||||
it("summarizes AI called/skipped/error state from step12_ai_repair", () => {
|
||||
const trace = buildAiTraceSummary(
|
||||
{
|
||||
step_status: "done",
|
||||
data: {
|
||||
per_unit: [
|
||||
{
|
||||
unit_index: 0,
|
||||
source_section_ids: ["04-1"],
|
||||
frame_template_id: "frame_reject",
|
||||
route_hint: "ai_adaptation_required",
|
||||
provisional: true,
|
||||
ai_called: false,
|
||||
skip_reason: "router_short_circuit",
|
||||
apply_status: "no_proposal",
|
||||
api_error_kind: null,
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
unit_index: 1,
|
||||
source_section_ids: ["04-2"],
|
||||
frame_template_id: "frame_auto",
|
||||
route_hint: "direct_render",
|
||||
provisional: false,
|
||||
ai_called: false,
|
||||
skip_reason: "not_provisional",
|
||||
apply_status: "no_proposal",
|
||||
api_error_kind: null,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
coverage_invariant: { status: "ok" },
|
||||
},
|
||||
},
|
||||
{
|
||||
status: "ok",
|
||||
counts: { total: 2, applied: 0, no_proposal: 2, no_zone_match: 0, unsupported_kind: 0, error: 0 },
|
||||
unsupported_kind_records: [],
|
||||
error_records: [],
|
||||
coverage_status: "ok",
|
||||
dropped_section_ids: [],
|
||||
human_review_required: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(trace.artifact_present).toBe(true);
|
||||
expect(trace.ai_enabled).toBe(false);
|
||||
expect(trace.ai_called_count).toBe(0);
|
||||
expect(trace.eligible_count).toBe(1);
|
||||
expect(trace.skip_reasons).toEqual({
|
||||
router_short_circuit: 1,
|
||||
not_provisional: 1,
|
||||
});
|
||||
expect(trace.warnings).toContain("router_short_circuit");
|
||||
expect(trace.units[0]).toMatchObject({
|
||||
source_section_ids: ["04-1"],
|
||||
route_hint: "ai_adaptation_required",
|
||||
provisional: true,
|
||||
ai_called: false,
|
||||
skip_reason: "router_short_circuit",
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces missing step12 AI artifact as an explicit warning", () => {
|
||||
const trace = buildAiTraceSummary(null, null);
|
||||
|
||||
expect(trace.artifact_present).toBe(false);
|
||||
expect(trace.ai_enabled).toBeNull();
|
||||
expect(trace.status).toBe("missing_artifact");
|
||||
expect(trace.warnings).toEqual(["step12_ai_repair.json missing"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const HOME_TSX = readFileSync(
|
||||
resolve(__dirname, "..", "src", "pages", "Home.tsx"),
|
||||
"utf-8",
|
||||
);
|
||||
const SLIDE_CANVAS_TSX = readFileSync(
|
||||
resolve(__dirname, "..", "src", "components", "SlideCanvas.tsx"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
function sliceHandleGenerateBody(source: string): string {
|
||||
const startMarker = "const handleGenerate = useCallback(async () =>";
|
||||
const startIdx = source.indexOf(startMarker);
|
||||
if (startIdx === -1) throw new Error("handleGenerate declaration not found");
|
||||
const afterStart = source.slice(startIdx + startMarker.length);
|
||||
const nextDeclIdx = afterStart.search(/\n {2}const [A-Za-z]/);
|
||||
return nextDeclIdx === -1 ? afterStart : afterStart.slice(0, nextDeclIdx);
|
||||
}
|
||||
|
||||
const HANDLE_GENERATE_BODY = sliceHandleGenerateBody(HOME_TSX);
|
||||
|
||||
describe("#98 Task 4 render stale guard", () => {
|
||||
it("clears prior runMeta before invoking runPipeline", () => {
|
||||
const clearIdx = HANDLE_GENERATE_BODY.indexOf("setRunMeta(null)");
|
||||
const loadingIdx = HANDLE_GENERATE_BODY.indexOf("isLoading: true");
|
||||
const runPipelineIdx = HANDLE_GENERATE_BODY.indexOf("runPipeline(");
|
||||
|
||||
expect(clearIdx).toBeGreaterThan(-1);
|
||||
expect(loadingIdx).toBeGreaterThan(-1);
|
||||
expect(runPipelineIdx).toBeGreaterThan(-1);
|
||||
expect(clearIdx).toBeLessThan(runPipelineIdx);
|
||||
expect(loadingIdx).toBeLessThan(runPipelineIdx);
|
||||
});
|
||||
|
||||
it("does not pass a finalHtmlUrl to SlideCanvas while a pipeline run is loading", () => {
|
||||
expect(HOME_TSX).toContain(
|
||||
"finalHtmlUrl={state.isLoading ? undefined : runMeta?.final_html_url}",
|
||||
);
|
||||
});
|
||||
|
||||
it("forces iframe remounts by keying the iframe on embeddedSrc", () => {
|
||||
expect(SLIDE_CANVAS_TSX).toContain("key={embeddedSrc}");
|
||||
});
|
||||
|
||||
it("resets iframe-derived overlay state on render url, loading, or pending-layout changes", () => {
|
||||
expect(SLIDE_CANVAS_TSX).toContain(
|
||||
"}, [finalHtmlUrl, isPipelineRunning, isPendingLayout]);",
|
||||
);
|
||||
expect(SLIDE_CANVAS_TSX).toContain("setMeasuredZones({});");
|
||||
expect(SLIDE_CANVAS_TSX).toContain("setMeasuredImages({});");
|
||||
expect(SLIDE_CANVAS_TSX).toContain("setSelectedImageId(null);");
|
||||
expect(SLIDE_CANVAS_TSX).toContain("setDragOverZoneId(null);");
|
||||
expect(SLIDE_CANVAS_TSX).toContain("setEditMode(\"off\");");
|
||||
});
|
||||
});
|
||||
@@ -181,6 +181,23 @@ describe("runPipeline reuseFromRunId forwarding (IMP-43 #72 u6)", () => {
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.run_id).toBe("test_run_id_20260524");
|
||||
});
|
||||
|
||||
it("includes ignoreUserOverrides only when clean generate requests it", async () => {
|
||||
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
||||
await runPipeline(
|
||||
makeFakeFile("03.mdx", "# title"),
|
||||
undefined,
|
||||
undefined,
|
||||
{ ignoreUserOverrides: true },
|
||||
);
|
||||
const cleanBody = lastPostBody();
|
||||
expect(cleanBody.ignoreUserOverrides).toBe(true);
|
||||
|
||||
fetchMock.mockResolvedValueOnce(mockResponse(SUCCESS_BODY));
|
||||
await runPipeline(makeFakeFile("03.mdx", "# title"));
|
||||
const normalBody = lastPostBody();
|
||||
expect("ignoreUserOverrides" in normalBody).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
@@ -196,9 +213,13 @@ describe("/api/run handler reuseFromRunId source-slice (IMP-43 #72 u6)", () => {
|
||||
expect(VITE_CONFIG_SOURCE).toMatch(/reuseFromRunId\?:\s*string\s*;/);
|
||||
});
|
||||
|
||||
it("declares ignoreUserOverrides?: boolean on the /api/run payload type", () => {
|
||||
expect(VITE_CONFIG_SOURCE).toMatch(/ignoreUserOverrides\?:\s*boolean\s*;/);
|
||||
});
|
||||
|
||||
it("destructures reuseFromRunId from payload alongside filename/content/overrides", () => {
|
||||
expect(VITE_CONFIG_SOURCE).toMatch(
|
||||
/const\s*\{\s*filename\s*,\s*content\s*,\s*overrides\s*,\s*reuseFromRunId\s*\}\s*=\s*payload\s*;/,
|
||||
/const\s*\{\s*filename\s*,\s*content\s*,\s*overrides\s*,\s*reuseFromRunId\s*,\s*ignoreUserOverrides\s*\}\s*=\s*payload\s*;/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -247,4 +268,11 @@ describe("/api/run handler reuseFromRunId source-slice (IMP-43 #72 u6)", () => {
|
||||
/cliArgs\.push\(\s*"--reuse-from"\s*,\s*reuseFromRunId\s*\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards --ignore-user-overrides only for clean generate requests", () => {
|
||||
const ignoreIdx = VITE_CONFIG_SOURCE.indexOf('"--ignore-user-overrides"');
|
||||
expect(ignoreIdx).toBeGreaterThan(-1);
|
||||
const guardWindow = VITE_CONFIG_SOURCE.slice(Math.max(0, ignoreIdx - 160), ignoreIdx);
|
||||
expect(guardWindow).toMatch(/ignoreUserOverrides\s*===\s*true/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
applyPersistedNonFrameOverrides,
|
||||
createInitialUserSelection,
|
||||
deriveUserOverridesKey,
|
||||
mergeSubmittedPipelineOverridesForRestore,
|
||||
remapPersistedFramesToZoneFrames,
|
||||
} from "../src/utils/slidePlanUtils";
|
||||
import type { SlidePlan, Zone } from "../src/types/designAgent";
|
||||
@@ -559,6 +560,65 @@ describe("restore-on-reopen end-to-end (IMP-52 u10)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("post-run restore prefers the submitted Generate payload over stale persisted axes", () => {
|
||||
const restoreDoc = mergeSubmittedPipelineOverridesForRestore(
|
||||
{
|
||||
layout: "vertical-2",
|
||||
frames: { "03-1": "stale_frame" },
|
||||
zone_geometries: { left: { x: 0, y: 0, w: 0.4, h: 1 } },
|
||||
zone_sections: { left: ["03-2"], right: ["03-1"] },
|
||||
manual_section_assignment: true,
|
||||
text_overrides: { top: { "title.0": "keep text edit" } },
|
||||
},
|
||||
{
|
||||
frames: { "03-1": "current_frame" },
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
expect(restoreDoc.layout).toBeUndefined();
|
||||
expect(restoreDoc.frames).toEqual({ "03-1": "current_frame" });
|
||||
expect(restoreDoc.zone_geometries).toBeUndefined();
|
||||
expect(restoreDoc.zone_sections).toBeUndefined();
|
||||
expect(restoreDoc.manual_section_assignment).toBe(false);
|
||||
expect(restoreDoc.text_overrides).toEqual({
|
||||
top: { "title.0": "keep text edit" },
|
||||
});
|
||||
});
|
||||
|
||||
it("converts the submitted Generate payload back to persisted-axis names for restore", () => {
|
||||
const restoreDoc = mergeSubmittedPipelineOverridesForRestore(
|
||||
{},
|
||||
{
|
||||
layout: "top-1-bottom-2",
|
||||
frames: { "01-intro": "three_parallel_requirements" },
|
||||
zoneGeometries: {
|
||||
top: { x: 0, y: 0, w: 1, h: 0.35 },
|
||||
bottom: { x: 0, y: 0.35, w: 1, h: 0.65 },
|
||||
},
|
||||
zoneSections: {
|
||||
top: ["01-intro"],
|
||||
bottom: ["01-1", "01-2"],
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(restoreDoc.layout).toBe("top-1-bottom-2");
|
||||
expect(restoreDoc.frames).toEqual({
|
||||
"01-intro": "three_parallel_requirements",
|
||||
});
|
||||
expect(restoreDoc.zone_geometries).toEqual({
|
||||
top: { x: 0, y: 0, w: 1, h: 0.35 },
|
||||
bottom: { x: 0, y: 0.35, w: 1, h: 0.65 },
|
||||
});
|
||||
expect(restoreDoc.zone_sections).toEqual({
|
||||
top: ["01-intro"],
|
||||
bottom: ["01-1", "01-2"],
|
||||
});
|
||||
expect(restoreDoc.manual_section_assignment).toBe(true);
|
||||
});
|
||||
|
||||
it("missing persisted file (GET returns {}) leaves the selection at backend defaults", async () => {
|
||||
fetchMock.mockResolvedValueOnce(mockResponse({}));
|
||||
const persisted = await getUserOverrides(deriveUserOverridesKey("new_file.mdx"));
|
||||
|
||||
@@ -777,6 +777,9 @@ function vitePluginPhaseZApi(): Plugin {
|
||||
// --reuse-from is supplied. Absent / empty = full pipeline
|
||||
// (byte-identical to pre-u6 spawn).
|
||||
reuseFromRunId?: string;
|
||||
// #98 Task 1 — clean generate / no-change regenerate must not
|
||||
// resurrect stale data/user_overrides/<key>.json fallback axes.
|
||||
ignoreUserOverrides?: boolean;
|
||||
};
|
||||
try {
|
||||
payload = JSON.parse(body);
|
||||
@@ -788,7 +791,7 @@ function vitePluginPhaseZApi(): Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
const { filename, content, overrides, reuseFromRunId } = payload;
|
||||
const { filename, content, overrides, reuseFromRunId, ignoreUserOverrides } = payload;
|
||||
if (!filename || typeof content !== "string") {
|
||||
res.writeHead(400, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
@@ -885,6 +888,9 @@ function vitePluginPhaseZApi(): Plugin {
|
||||
if (reuseFromRunId && typeof reuseFromRunId === "string") {
|
||||
cliArgs.push("--reuse-from", reuseFromRunId);
|
||||
}
|
||||
if (ignoreUserOverrides === true) {
|
||||
cliArgs.push("--ignore-user-overrides");
|
||||
}
|
||||
console.log(
|
||||
`[phase-z-api] spawn pipeline: run_id=${runId}, mdx=${mdxPath}, args=${JSON.stringify(cliArgs.slice(2))}`
|
||||
);
|
||||
@@ -954,11 +960,13 @@ function vitePluginPhaseZApi(): Plugin {
|
||||
});
|
||||
});
|
||||
|
||||
// ── GET /api/sample-mdx → samples/mdx/{03|04|05} (데모) ──
|
||||
// ── GET /api/sample-mdx → samples/mdx/{01|02|03|04|05} (데모) ──
|
||||
// 페이지 로드 시 frontend 가 자동 fetch — 상대방에게 mdx 파일 공유 안 해도 되게.
|
||||
// 2026-05-14 — query param `?mdx=04` / `?mdx=05` 추가 (default = 03).
|
||||
// 04 / 05 도 frontend simulation 가능하도록 frame/layout override 적용 path 유지.
|
||||
// 2026-05-14 — query param `?mdx=01`~`?mdx=05` 추가 (default = 03).
|
||||
// 01~05 모두 frontend simulation 가능하도록 frame/layout override 적용 path 유지.
|
||||
const SAMPLE_MDX_MAP: Record<string, string> = {
|
||||
"01": "01. 건설산업 DX의 올바른 이해(0127).mdx",
|
||||
"02": "02. DX의 시행 목표 및 기대효과.mdx",
|
||||
"03": "03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx",
|
||||
"04": "04. DX 지연 요인.mdx",
|
||||
"05": "05. 설계 방식의 왜곡.mdx",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# IMP-46 u6 — persistent frame_cache root marker.
|
||||
#
|
||||
# Tracked so the directory exists on a fresh checkout; cached JSON payloads
|
||||
# under data/frame_cache/{frame_id}/{signature_hash}.json remain git-ignored
|
||||
# via the .gitignore exception block.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Five-MDX Structure Inventory
|
||||
|
||||
Issue: #98
|
||||
|
||||
Purpose: establish the empirical ground truth for T22-T30 before changing
|
||||
the parser, matcher, layout planner, slot mapper, frontend override path, or
|
||||
AI adaptation path.
|
||||
|
||||
This inventory intentionally compares three layers:
|
||||
|
||||
| Column | Meaning | Gap type exposed |
|
||||
|---|---|---|
|
||||
| A. Original MDX contains | What the source file actually expresses | Source structure |
|
||||
| B. Current pipeline sees | What the current `task21d_*` runs expose in Step 02 / Step 06 / Step 20 | Parser, normalizer, component, and planner behavior |
|
||||
| C. Expected result | What the slide pipeline should preserve and render | Layout, matching, slot mapping, override, and adaptation behavior |
|
||||
|
||||
Source files inspected:
|
||||
|
||||
- `samples/mdx/01. ... .mdx`
|
||||
- `samples/mdx/02. ... .mdx`
|
||||
- `samples/mdx/03. ... .mdx`
|
||||
- `samples/mdx/04. ... .mdx`
|
||||
- `samples/mdx/05. ... .mdx`
|
||||
|
||||
Observed pipeline runs:
|
||||
|
||||
- `data/runs/task21d_01`
|
||||
- `data/runs/task21d_02`
|
||||
- `data/runs/task21d_03`
|
||||
- `data/runs/task21d_04`
|
||||
- `data/runs/task21d_05`
|
||||
|
||||
## MDX 01
|
||||
|
||||
| A. Original MDX contains | B. Current pipeline sees | C. Expected result | Gap owner |
|
||||
|---|---|---|---|
|
||||
| A pre-intro block before the first numbered `##` heading. It includes a details/popup block and is user-visible content. | Step 02 only emits `01-1` and `01-2`. The pre-intro has no unit id. Existing `data/user_overrides/01.json` references `01-3`, but that id is orphaned. | Promote the pre-intro into a first-class unit such as `01-intro`, or define a documented alias/migration path for the existing `01-3` override. | T22 parser/normalizer and override migration |
|
||||
| `## 1` and `## 2` numbered sections. | Step 06 selects two units: `01-1 -> construction_bim_three_usage`, `01-2 -> bim_dx_comparison_table`. | Body zones should be based on the normalized section tree after the pre-intro policy is decided. | T24 layout policy |
|
||||
| An image reference in section 2 and details/popup content. | Recent asset handling removes broken image icons, but popup/details semantics remain only partially represented. | Image and popup/details nodes must be explicit node types, not raw markdown leftovers. | T22 node taxonomy and T25 slot mapping |
|
||||
|
||||
## MDX 02
|
||||
|
||||
| A. Original MDX contains | B. Current pipeline sees | C. Expected result | Gap owner |
|
||||
|---|---|---|---|
|
||||
| `## 1` with goal content and one image. | Step 02 emits `02-1`. Step 06 selects `construction_goals_three_circle_intersection`. A user override exists for `02-1`, but frontend/runtime override behavior is not proven end-to-end. | Section `02-1` must remain independently addressable, and selected/overridden frames must render without stale preview or silent fallback. | T28 frontend override E2E |
|
||||
| `## 2`, with `### 2.1` and `### 2.2`. | Step 02 emits only parent `02-2`; child headings are raw content inside the section. | `2.1` and `2.2` should become frame-internal groups under the `02-2` unit, not separate slide zones unless a layout policy explicitly asks for that. | T22/T25 |
|
||||
| `<DxEffect />`, which represents the stakeholder/persona effect table in `samples/src/components/dx.astro`. | The pipeline sees an opaque component tag; persona columns are not expanded into semantic nodes. | Register `DxEffect` in a component expansion registry and expand it into persona/table nodes before matching and slot mapping. | T23 component expansion |
|
||||
|
||||
## MDX 03
|
||||
|
||||
| A. Original MDX contains | B. Current pipeline sees | C. Expected result | Gap owner |
|
||||
|---|---|---|---|
|
||||
| `## 1` with three conceptual groups: technology, people, and environment/nature. | Step 06 selects `three_parallel_requirements` for `03-1`. Technical text coverage is currently PASS. | Child groups should populate the internal pillar labels and body slots of the selected frame. They must not become external slide titles. | T25 slot mapping |
|
||||
| `## 2` with `### 2.1` process and `### 2.2` product, including table-like content. | Step 06 selects `process_product_two_way` for `03-2`. | Process/product child groups should map to the selected two-way frame with all source atoms preserved. | T25/T27 |
|
||||
| Dense content in two zones. | Presentation can still become visually dense even when technical coverage is PASS. | Fit must be solved through zone sizing, frame expansion, or layout choice, not font shrinking or text truncation. | T27 and T30 |
|
||||
|
||||
## MDX 04
|
||||
|
||||
| A. Original MDX contains | B. Current pipeline sees | C. Expected result | Gap owner |
|
||||
|---|---|---|---|
|
||||
| `## 1` with several perception/problem groups. | Step 06 selects `pre_construction_model_info_stacked` with a reject/provisional path. Step 20 marks design readiness as `needs_review`. | Reject/provisional candidates must remain visible as candidates, but the slide should surface that adaptation is required. | T26 candidate policy |
|
||||
| `## 2` with `### 2.1` and `### 2.2`, plus details blocks. | Step 06 keeps parent `04-2` and selects `bim_issues_quadrant_four`; earlier runs showed child handling instability. | Child groups under `04-2` should become internal quadrant/list items in source order. | T25 slot mapping |
|
||||
| Content is larger than the currently chosen zones in some layouts. | Technical PASS does not guarantee presentation readiness. | The system should resize/replan zones or expand frame structure. It must not shrink font size to hide overflow. | T24/T27/T30 |
|
||||
|
||||
## MDX 05
|
||||
|
||||
| A. Original MDX contains | B. Current pipeline sees | C. Expected result | Gap owner |
|
||||
|---|---|---|---|
|
||||
| Two top-level numbered sections, `05-1` and `05-2`. | Step 06 now emits two units, but both use `three_parallel_requirements` through `generic_fallback`. Step 20 marks design readiness as `not_ready`. | Sections must stay separated, and fallback/generic frames must be visible as not ready unless a real matching/adaptation path is used. | T24/T26 |
|
||||
| `05-2` uses numeric `### 1` and `### 2` headings, not decimal `### 2.1` style headings. | These child headings are raw section content. | The contract must decide whether numeric child headings are allowed input or require MDX standardization. | T21.6/T22 |
|
||||
| User-selected alternative frames can work through CLI override, but UI selection has not been proven. | `data/user_overrides/05.json` contains zone assignment but no `frames` override. | Frontend frame selection must persist `frames` and backend must apply the selected frame without stale preview or silent no-op. | T28 frontend override E2E |
|
||||
|
||||
## Aggregate Node Inventory
|
||||
|
||||
| Node type | Found in | Current state | Contract decision needed |
|
||||
|---|---|---|---|
|
||||
| `pre_intro` | MDX 01 | Not promoted to a unit | Allow as first-class unit or require source standardization |
|
||||
| `numbered_section` | MDX 01-05 | Parsed as top-level units | Keep as the main slide-zone unit type |
|
||||
| `subsection_decimal` | MDX 02, 03, 04 | Usually raw content under parent | Map to frame-internal groups unless explicitly split |
|
||||
| `subsection_numeric` | MDX 05 | Raw content under parent | Decide whether allowed or normalized to decimal form |
|
||||
| `component_ref` | MDX 02 `<DxEffect />` | Opaque tag | Registry-based component expansion |
|
||||
| `image_ref` | MDX 01, MDX 02 | Asset copy now works for known frame images; content images still need typed handling | Explicit image node and image slot policy |
|
||||
| `details_popup` | MDX 01, MDX 04 | Raw markdown/html-like content | Explicit popup/details node and presentation policy |
|
||||
| `table_like_content` | MDX 01 details, MDX 02 component, MDX 03 table | Partly raw content | Explicit table/comparison nodes |
|
||||
| `summary_footer` | All five MDX samples conceptually | Must stay outside body frame zones | Fixed base-slide region, not a body layout zone |
|
||||
|
||||
## Aggregate Gap List
|
||||
|
||||
| Gap | Evidence | Next task |
|
||||
|---|---|---|
|
||||
| Pre-intro content lacks stable unit identity | MDX 01 source has visible content before the first `##`; Step 02 emits only `01-1` and `01-2`; override references orphan `01-3` | T22 |
|
||||
| Registered component expansion does not exist | MDX 02 uses `<DxEffect />`; current parser sees only the component tag | T23 |
|
||||
| Child headings need frame-internal mapping | MDX 02/03/04/05 child headings remain raw content or map inconsistently | T25 |
|
||||
| Generic fallback can be technically PASS but design-not-ready | MDX 05 uses `generic_fallback` for both units | T26/T30 |
|
||||
| Frontend override persistence is not proven | MDX 05 override JSON has no `frames`; CLI override works separately | T28 |
|
||||
| Fit is still a structure/layout problem, not a font problem | MDX 03/04 can be dense; policy forbids font shrinking | T24/T27/T30 |
|
||||
|
||||
## Immediate Contract Decisions For T21.6
|
||||
|
||||
1. Unit identity policy:
|
||||
- Prefer stable explicit ids such as `01-intro`, not implicit renumbering.
|
||||
- Existing user override aliases such as `01-3` need a tested migration or compatibility rule.
|
||||
2. Component registry:
|
||||
- Start with `DxEffect`.
|
||||
- New components must be added through registry entries, not one-off parser branches.
|
||||
3. Child-section policy:
|
||||
- Top-level `## n.` sections map to body zones.
|
||||
- Child headings map to internal frame slots by default.
|
||||
4. Allowed input policy:
|
||||
- `## n. Title` is allowed.
|
||||
- Registered component tags are allowed.
|
||||
- Unregistered component tags are unsupported.
|
||||
- Raw JSX/style blobs are disallowed in rendered text and must be sanitized or rejected.
|
||||
5. Coverage-state policy:
|
||||
- `covered_native`: current frame and builder can render source atoms verbatim.
|
||||
- `covered_via_expand`: current frame can render after deterministic repeat/expand.
|
||||
- `requires_adaptation`: visual motif is usable, but an adapted schema is needed.
|
||||
- `unsupported`: no current contract path; must be surfaced honestly.
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# MDX-to-Frame Structure Contract
|
||||
|
||||
Issue: #98
|
||||
|
||||
Status: draft contract for T22-T30.
|
||||
|
||||
This document converts `5_MDX_STRUCTURE_INVENTORY.md` into a shared
|
||||
contract for parser normalization, component expansion, layout selection,
|
||||
frame candidate ranking, slot mapping, frontend override behavior, and AI
|
||||
adaptation.
|
||||
|
||||
The contract is deliberately process-first. It must not introduce MDX-id
|
||||
special cases, generated text, font shrinking, or silent fallback.
|
||||
|
||||
## 1. Non-Negotiable Policies
|
||||
|
||||
| Policy | Rule |
|
||||
|---|---|
|
||||
| Source text preservation | MDX source atoms are copied verbatim into rendered slots. No omission, summary, inference, or rewriting. |
|
||||
| AI isolation | AI may propose adapted frame structure only. AI must not output new or modified MDX text. |
|
||||
| Unit separation | Top-level content units must not be silently merged. |
|
||||
| Child mapping | Child headings and groups usually map to frame-internal labels/body slots, not independent slide titles. |
|
||||
| Summary/footer | Summary/footer content belongs to the fixed base-slide region, not body layout zones. |
|
||||
| Fit policy | Do not reduce font size to force fit. Use layout, zone sizing, frame expansion, or needs-review state. |
|
||||
| Candidate honesty | Reject/provisional candidates remain visible with status. They are not deleted and not treated as ready. |
|
||||
| No silent swap | If selected frame and rendered frame differ, record a failure or explicit adaptation state. |
|
||||
| No debug leakage | Internal ids, counters, and pipeline labels must not appear in presentation output. |
|
||||
|
||||
## 2. Node Taxonomy
|
||||
|
||||
| Node type | Definition | Unit role | Examples from inventory |
|
||||
|---|---|---|---|
|
||||
| `pre_intro` | Visible content before the first numbered `##` heading | Optional top-level unit | MDX 01 intro/details before `## 1` |
|
||||
| `numbered_section` | `## n. Title` top-level section | Default slide body unit | MDX 01-05 |
|
||||
| `subsection_decimal` | `### n.m Title` child section | Frame-internal group by default | MDX 02, 03, 04 |
|
||||
| `subsection_numeric` | `### n Title` child section without parent decimal prefix | Allowed only if normalized to parent-scoped child id | MDX 05 |
|
||||
| `bullet_group` | Consecutive bullet lines under a unit or child heading | Slot body group | MDX 01-05 |
|
||||
| `table_like_content` | Markdown table, component-expanded table, or details table | Table/comparison slot group | MDX 01 details, MDX 02 `DxEffect`, MDX 03 |
|
||||
| `image_ref` | Markdown image reference with alt text and src | Image slot | MDX 01, MDX 02 |
|
||||
| `component_ref` | Registered MDX/Astro component tag | Expanded node tree | MDX 02 `<DxEffect />` |
|
||||
| `details_popup` | `<details>` block or equivalent collapsible detail source | Popup/detail body slot or auxiliary note | MDX 01, MDX 04 |
|
||||
| `summary_footer` | Closing summary or key message outside body frame zones | Fixed footer/base-slide region | All five samples conceptually |
|
||||
|
||||
## 3. Unit Identity Rules
|
||||
|
||||
| Input pattern | Unit id policy | Notes |
|
||||
|---|---|---|
|
||||
| `pre_intro` before first numbered section | `<mdx_id>-intro` | Existing orphan overrides such as `01-3` need alias migration to this id. |
|
||||
| `## n. Title` | `<mdx_id>-<n>` | This remains the stable primary unit id. |
|
||||
| `### n.m Title` under parent `n` | `<mdx_id>-<n>-sub-<m>` if materialized, otherwise child node under parent | Materialization must be explicit; default is internal child node. |
|
||||
| `### k Title` under parent `n` | `<mdx_id>-<n>-sub-<k>` after parent-scoped normalization | This covers MDX 05 numeric child headings. |
|
||||
| Registered component | Stays under containing unit unless registry declares child units | `DxEffect` should expand under `02-2`. |
|
||||
|
||||
Override compatibility:
|
||||
|
||||
- Existing overrides that reference missing ids must not be silently ignored.
|
||||
- A compatibility alias table must be emitted during T22 when a prior id can be
|
||||
mapped safely, such as `01-3 -> 01-intro` if that is confirmed as user
|
||||
intent.
|
||||
- If an override id cannot be resolved, the frontend and pipeline trace must
|
||||
show `override_unresolved`.
|
||||
|
||||
## 4. Slot Taxonomy
|
||||
|
||||
| Slot type | Meaning | Text source |
|
||||
|---|---|---|
|
||||
| `title` | Top-level frame title | Unit title only |
|
||||
| `label` | Short internal group label | Child heading or source label atom only |
|
||||
| `body_text_lines` | Paragraph or bullet text | Source atoms only |
|
||||
| `pillar` | Generic repeated vertical group with label/body | Child heading plus child body |
|
||||
| `persona_column` | Stakeholder/persona column | Component-expanded table/persona node |
|
||||
| `comparison_cell` | Cell in two-column or multi-column comparison | Source table cell or bullet group |
|
||||
| `table_row` | Repeated row in a table-like frame | Source row or key/value atom |
|
||||
| `image_target` | Frame image slot | Source image_ref, never decorative generated image |
|
||||
| `popup_body` | Detail/popup auxiliary content | Source details content |
|
||||
| `footer_summary` | Fixed summary/footer region | Summary source only |
|
||||
|
||||
`pillar` is a generic repeated group concept. `three_parallel_requirements`
|
||||
uses it as one concrete family implementation, but the slot type itself is not
|
||||
limited to that frame.
|
||||
|
||||
## 5. Node-to-Slot Mapping Matrix
|
||||
|
||||
| Node type | Allowed target slots | Default behavior | Disallowed behavior |
|
||||
|---|---|---|---|
|
||||
| `pre_intro` | `title`, `label`, `body_text_lines`, `popup_body` | Treat as a top-level unit when visible and substantial | Dropping it because it precedes `## 1` |
|
||||
| `numbered_section` | `title`, `body_text_lines`, `image_target`, `table_row` | One body zone unit by default | Merging with another top-level section without explicit user action |
|
||||
| `subsection_decimal` | `label`, `body_text_lines`, `pillar`, `comparison_cell`, `table_row` | Internal frame group | Using child title as the whole slide title when parent is the unit |
|
||||
| `subsection_numeric` | `label`, `body_text_lines`, `pillar` | Normalize under current parent | Treating it as a new top-level section |
|
||||
| `bullet_group` | `body_text_lines`, `comparison_cell`, `table_row` | Preserve bullet text verbatim | Rewriting or summarizing |
|
||||
| `table_like_content` | `table_row`, `comparison_cell`, `persona_column` | Preserve cells verbatim | Flattening into prose without trace |
|
||||
| `image_ref` | `image_target` | Resolve source image path and alt text | Broken image icon or decorative replacement |
|
||||
| `component_ref` | Registry-defined expanded nodes | Expand before matching | Passing opaque tag into rendered text |
|
||||
| `details_popup` | `popup_body`, optional note slot | Preserve hidden/detail text in trace and supported frames | Leaking raw `<details>` markup as prose |
|
||||
| `summary_footer` | `footer_summary` | Fixed base-slide region | Consuming a body layout zone |
|
||||
|
||||
## 6. Component Expansion Registry Contract
|
||||
|
||||
Registry file:
|
||||
|
||||
- `templates/phase_z2/catalog/component_expansion_registry.yaml`
|
||||
|
||||
Initial required entry:
|
||||
|
||||
- `DxEffect`
|
||||
|
||||
Rules:
|
||||
|
||||
- Registered components expand into typed nodes before matching.
|
||||
- Unregistered component tags are `unsupported`, not silently rendered as text.
|
||||
- Component expansion is data-driven through the registry, not a one-off parser
|
||||
branch.
|
||||
- Expanded text still follows source preservation: copied from component source,
|
||||
not generated.
|
||||
|
||||
## 7. Coverage-State Taxonomy
|
||||
|
||||
The same state labels must be used by Step 05/06/20, frontend candidate panel,
|
||||
and AI adaptation routing.
|
||||
|
||||
| State | Meaning | User-facing behavior | AI behavior |
|
||||
|---|---|---|---|
|
||||
| `covered_native` | Existing frame and builder can render all required source atoms verbatim | Candidate is selectable and renderable | No AI |
|
||||
| `covered_via_expand` | Existing frame can render after deterministic repeat/expand | Candidate is selectable with expansion note | No AI |
|
||||
| `requires_adaptation` | Visual motif is usable, but slot schema/repeat structure must be adapted | Candidate is selectable with AI-adaptation badge | AI may propose adapted schema only |
|
||||
| `unsupported` | No current contract path | Show unsupported / needs design asset | No automatic AI text or hidden fallback |
|
||||
|
||||
Reject/provisional V4 labels are not deleted. They map to
|
||||
`requires_adaptation` or `unsupported` depending on whether the frame motif can
|
||||
be structurally reused.
|
||||
|
||||
## 8. Allowed And Disallowed Input Rules
|
||||
|
||||
| Input | Contract status | Action |
|
||||
|---|---|---|
|
||||
| `## n. Title` | Allowed | Parse as `numbered_section`. |
|
||||
| Visible content before first `##` | Allowed if non-empty | Parse as `pre_intro`. |
|
||||
| `### n.m Title` under current parent | Allowed | Parse as `subsection_decimal`. |
|
||||
| `### k Title` under current parent | Allowed with normalization | Parse as `subsection_numeric`; emit normalized id. |
|
||||
| Registered component tag | Allowed | Expand through registry. |
|
||||
| Unregistered component tag | Unsupported | Surface unsupported with trace. |
|
||||
| Raw JSX style object or event handler in source text | Disallowed in rendered text | Sanitize or fail coverage gate. |
|
||||
| Raw HTML details block | Allowed with typed handling | Parse as `details_popup`. |
|
||||
|
||||
## 9. Frame Coverage Priority
|
||||
|
||||
T27 must not attempt to migrate all 32 contracts at once. The priority is:
|
||||
|
||||
1. Frames actually selected or requested by the five MDX samples.
|
||||
2. Frames with existing family partials under `templates/phase_z2/families`.
|
||||
3. Remaining catalog-only frames, tracked separately as a family/variant mapping
|
||||
axis. This remains separate from the #98 immediate readiness work.
|
||||
|
||||
Initial frame families in scope:
|
||||
|
||||
- `construction_bim_three_usage`
|
||||
- `bim_dx_comparison_table`
|
||||
- `construction_goals_three_circle_intersection`
|
||||
- `three_persona_benefits`
|
||||
- `three_parallel_requirements`
|
||||
- `process_product_two_way`
|
||||
- `pre_construction_model_info_stacked`
|
||||
- `bim_issues_quadrant_four`
|
||||
|
||||
## 10. Task Mapping
|
||||
|
||||
| Task | Contract responsibility |
|
||||
|---|---|
|
||||
| T22 | Implement node taxonomy and unit identity rules. |
|
||||
| T23 | Implement component expansion registry, starting with `DxEffect`. |
|
||||
| T24 | Select layout from normalized top-level units and fixed summary/footer policy. |
|
||||
| T25 | Implement node-to-slot mapping and builder coverage checks. |
|
||||
| T26 | Carry candidate pools and coverage states through backend and frontend. |
|
||||
| T27 | Add deterministic repeat/expand rules without font shrinking. |
|
||||
| T28 | Verify frontend override persistence, stale-preview prevention, and backend application. |
|
||||
| T29 | Implement AI structure-plan path only for `requires_adaptation`. |
|
||||
| T30 | Run five-MDX technical, design-readiness, and presentation-readiness gates. |
|
||||
|
||||
## 11. Acceptance Checks For This Contract
|
||||
|
||||
- The contract defines node taxonomy, slot taxonomy, node-to-slot mapping,
|
||||
coverage states, allowed/disallowed input rules, and task mapping.
|
||||
- The contract explicitly covers all five MDX samples from the inventory.
|
||||
- The contract does not permit AI text generation or font shrinking.
|
||||
- The contract defines how reject/provisional candidates are surfaced without
|
||||
silent deletion.
|
||||
- The contract identifies registry files that later implementation tasks can
|
||||
read.
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
# Emergency Session — mdx 01~05 visible-result rescue
|
||||
|
||||
**시작**: 2026-05-26
|
||||
**목표 시간**: ~1 시간
|
||||
**모드**: 로컬 코드 개선 (no Gitea push, no orchestrator, no issue 생성)
|
||||
|
||||
---
|
||||
|
||||
## 1. 목표
|
||||
|
||||
프론트에서 mdx 01~05 **5 개 모두 반드시 슬라이드 출력** (empty shell 금지) + 사용자가 frame 선택/변경 가능 + 선택된 frame 으로 AI 가 원문 보존하며 재배치.
|
||||
|
||||
### 정책 lock (한 줄)
|
||||
|
||||
> **empty shell 금지. 어떤 경우든 원문 텍스트 보존 + 선택 가능 frame 또는 fallback frame 에 AI 가 재배치.**
|
||||
|
||||
**현재 상태**:
|
||||
| mdx | 결과 |
|
||||
|---|---|
|
||||
| 03 | ✅ direct path — 정상 |
|
||||
| 04 | ❌ 04-1 reject → provisional, 04-2 plan 에서 누락. 1 slide 만 |
|
||||
| 05 | ❌ 05-1 + 05-2 empty_shell. 빈 placeholder |
|
||||
| 01, 02 | unknown (이번 세션 sanity check 필요) |
|
||||
|
||||
**기대 (정책 적용 후)**: 모든 mdx 의 모든 section 이 슬라이드로 렌더 — reject-only 여도 AI 재배치 path 로 surface.
|
||||
|
||||
---
|
||||
|
||||
## 2. 운영 원칙 (lock)
|
||||
|
||||
- ❌ git push 금지
|
||||
- ❌ Gitea issue 생성 / orchestrator 실행 금지
|
||||
- ❌ 결과물 하드코딩 금지 (mdx 별 특수 처리 X)
|
||||
- ❌ MDX 원문 텍스트 변경 금지
|
||||
- ❌ 후보 수 강제 floor (3~6 padding) 금지
|
||||
- ❌ **empty_shell 종결 금지** — 후보 reject-only 라도 AI adaptation route 필수
|
||||
- ❌ **reject 후보 자동 렌더 금지** — 항상 ai_adaptation_required 로 status 부여
|
||||
- ✅ 코드 path / 정책 / fallback 수정
|
||||
- ✅ 각 edit 후 로컬 run 으로 검증
|
||||
- ✅ Codex + 사용자 + Claude consensus 매 step
|
||||
|
||||
---
|
||||
|
||||
## 3. Root cause 진단 (확정)
|
||||
|
||||
3-layer contradiction:
|
||||
|
||||
```
|
||||
Layer 1: vite.config.ts spawn env
|
||||
PHASE_Z_ALLOW_REJECT="1" ← 의도: reject 허용
|
||||
← 실제: src/ 에서 read 안 됨 (dead env var)
|
||||
|
||||
Layer 2: src/phase_z2_pipeline.py:1481-1520
|
||||
lookup_v4_non_reject_candidates()
|
||||
if j.get("label") == "reject": continue ← reject 강제 drop (May 8 lock)
|
||||
|
||||
Layer 3: composition + IMP-30 u4
|
||||
selection_path = "empty_shell"
|
||||
imp30_u4 = "terminal_first_render_empty_shell" ← AI fallback 도달 차단
|
||||
```
|
||||
|
||||
**결과**: reject 후보가 있어도 → filter 에서 drop → empty_shell → terminal → AI fallback 미호출.
|
||||
|
||||
**Cross-reference**:
|
||||
- `.env: AI_FALLBACK_ENABLED=true` ✅ 활성
|
||||
- 사용자 정책 ([feedback_ai_isolation_contract]): "reject = AI 재구성 대상 (frame 유지 + AI 재배치)"
|
||||
- IMP-47B (#76): reject 후보 surface = override path 만 (자동 path 미포함)
|
||||
|
||||
### Resolution — 통합 정책
|
||||
|
||||
3-layer contradiction 의 해소 = **§1 의 정책 lock ("empty shell 금지")** 적용 :
|
||||
|
||||
1. Layer 2 의 reject filter → **status 분류로 대체** (제거 X, 모두 보존 + status 부여)
|
||||
2. Layer 3 의 empty_shell terminal → **제거** (어떤 후보 상태든 AI adaptation route 로 흐름)
|
||||
3. Layer 1 의 dead env → wire-up 또는 retire (§4 Quick fix)
|
||||
|
||||
### P1 결과 (2026-05-26) — 3 종류 root cause 동시 존재 (단순 reject filter X)
|
||||
|
||||
P1 검증으로 처음 가정 ("주로 reject filter 문제") 와 실제 가 다름 발견 :
|
||||
|
||||
| 분류 | 영향 section | 처리 priority |
|
||||
|---|---|---|
|
||||
| **C1. All-reject (catalog gap)** | `02-2.1`, `04-1` (raw 32 모두 reject) | **P2** — reject = `ai_adaptation_required` (status 분류) |
|
||||
| **C2. Section key mismatch** | `02-2` ↔ `02-2.1/02-2.2`, `04-2` ↔ `04-2.1/04-2.2` (V4 source 가 split) | **P2.5** — parent ↔ child key alignment policy (universal) |
|
||||
| **C3. V4 source 자체 누락** | `mdx 05 전체` (entry 0) | **P3** — generic fallback frame mandatory (raw 후보 0 일반 정책) |
|
||||
|
||||
**fixture 주의** : `tests/matching/v4_full32_result.yaml` = 2026-04-29 stale snapshot. 이번 세션 = fallback policy 강화로 fixture 의존 약화. runtime V4 evaluator migrate = 별 axis (이번 세션 후).
|
||||
|
||||
### Candidate status 3 분류 (정책 기반)
|
||||
|
||||
| status | 후보 label | 처리 |
|
||||
|---|---|---|
|
||||
| `auto_renderable` | use_as_is / light_edit / restructure (일부) | 즉시 직접 렌더 가능 |
|
||||
| `ai_adaptation_required` | needs_adaptation / reject / fallback_candidate | 자동 렌더 X, AI 재배치 필요 |
|
||||
| `blocked_runtime` | visual_pending / catalog scaffolding 등 | 현재 시스템상 렌더 불가 (별 axis 로 추적) |
|
||||
|
||||
**규칙**:
|
||||
- `auto_renderable` 1+ → 그 중 rank-1 으로 직접 렌더
|
||||
- `auto_renderable` 0 + `ai_adaptation_required` 1+ → top 후보 AI adaptation route
|
||||
- 둘 다 0 → generic_fallback frame + AI adaptation (별 axis — 이번 세션 후순위, §4 결정 보류 참조)
|
||||
- **어떤 경우든 empty_shell 종결 X**
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 순서 (5 priority + 1 quick fix)
|
||||
|
||||
### Priority 1 — Raw 후보 분포 확인 (5~10분, read-only)
|
||||
|
||||
**목적**: catalog gap (후보 없음) vs matching gap (후보 있지만 reject) 구분.
|
||||
|
||||
**파일**:
|
||||
- `tests/matching/v4_full32_result.yaml` (raw 32 candidates source)
|
||||
- `data/runs/mdx04_clean_check_v2/phase_z2/steps/step05_v4_evidence.json`
|
||||
- `data/runs/mdx05_clean_check_v2/phase_z2/steps/step05_v4_evidence.json`
|
||||
|
||||
**검증 질문**:
|
||||
- 04-1, 04-2, 05-1, 05-2 각각의 raw 32 candidates 중 reject 수 / non-reject 수?
|
||||
- 어떤 frame_template_id 들이 reject 됐나? (catalog 의 가능성)
|
||||
- candidate_status="no_non_reject_v4_candidate" 가 정말 모든 4 섹션?
|
||||
|
||||
### Priority 2 — V4 후보 보존 + status 분류 (15~20분, edit)
|
||||
|
||||
**목적**: reject 후보 삭제 X, 모두 보존 + `candidate_status` 부여 (auto_renderable / ai_adaptation_required / blocked_runtime).
|
||||
|
||||
**파일 + 함수**:
|
||||
- `src/phase_z2_pipeline.py:1481-1520` `lookup_v4_non_reject_candidates`
|
||||
- rename → `lookup_v4_candidates(v4, section_id, max_n=6, include_reject=True, alias_keys=None)`
|
||||
- **default = `include_reject=True`** (정책 lock 반영)
|
||||
- 모든 후보 반환 + 각 V4Match 에 `candidate_status` field 추가
|
||||
- reject filter 제거 — visual_pending 만 `blocked_runtime` 로 표시 (drop X)
|
||||
|
||||
**V4Match dataclass / dict 확장**:
|
||||
- `candidate_status: str` field 추가
|
||||
- 분류 규칙 (단일 source of truth):
|
||||
- label ∈ {use_as_is, light_edit} → `auto_renderable`
|
||||
- label = restructure → `auto_renderable` (단, capacity_fit 통과 시) / `ai_adaptation_required` (mismatch 시)
|
||||
- label ∈ {needs_adaptation, reject, fallback_candidate} → `ai_adaptation_required`
|
||||
- visual_pending = True → `blocked_runtime`
|
||||
|
||||
**호출처 검색 필요**:
|
||||
- `grep -n lookup_v4_non_reject_candidates src/`
|
||||
- 호출처 마다 status field 사용 여부 확인 + 필요 시 분기 추가
|
||||
|
||||
**composition 변경**:
|
||||
- `src/phase_z2_composition.py` (line ~681 부근)
|
||||
- 후보 retrieve → status 별 분류:
|
||||
- `auto_renderable` 1+ → rank-1 으로 직접 렌더
|
||||
- 그 외 → `ai_adaptation_required` route (P3 의 empty_shell 우회 와 연결)
|
||||
|
||||
### Priority 2.5 — Section key alignment policy (10~15분, edit) [NEW — C2 처리]
|
||||
|
||||
**목적**: pipeline normalize 의 section_id (예: `04-2`) 와 V4 source 의 split child keys (예: `04-2.1`, `04-2.2`) 매칭. **universal policy** — 02-2 / 04-2 모두 cover, mdx 별 dispatch X.
|
||||
|
||||
**접근 — "child → parent pool merge" (Codex 안)** :
|
||||
parent 를 child 로 split 하는 방식 (composition planner 변경) 보다 빠르고 안전 :
|
||||
|
||||
```
|
||||
section_id = X (e.g., "04-2")
|
||||
lookup_v4_candidates(X) → V4 에 X 직접 매칭 시도
|
||||
hit → return as-is
|
||||
miss → fallback: child keys 검색 (X.1, X.2, X.3, ...)
|
||||
hit → 모든 child candidates 를 X 의 candidate pool 로 merge
|
||||
(origin: child_id 보존 — debug)
|
||||
miss → return [] (P3 의 generic fallback 으로 흐름)
|
||||
```
|
||||
|
||||
**파일**:
|
||||
- `src/phase_z2_pipeline.py:1505` `_resolve_v4_section_key` — alias 기능 이미 일부 있음 (alias_keys param). 확장 필요.
|
||||
- `lookup_v4_candidates` (P2 에서 신설) — alias miss 시 child key scan 추가
|
||||
|
||||
**검증 sample**:
|
||||
- `04-2` lookup → V4 miss → `04-2.1` + `04-2.2` 찾음 → 5 candidates pool 로 merge (04-2.1=2, 04-2.2=3)
|
||||
- `02-2` lookup → V4 miss → `02-2.1` + `02-2.2` 찾음 → 5 candidates pool
|
||||
|
||||
### Priority 3 — empty_shell terminal 제거 + AI adaptation + generic fallback mandatory (15~20분, edit)
|
||||
|
||||
**목적**: `terminal_first_render_empty_shell` (IMP-30 u4) **제거** — 어떤 후보 상태든 AI adaptation 으로 분기. **empty_shell 종결 = 0 보장**. mdx 05 같은 raw 후보 0 케이스도 cover (C3).
|
||||
|
||||
**파일**:
|
||||
- `src/phase_z2_composition.py` 또는 `src/phase_z2_pipeline.py` 의 IMP-30 u4 분기점
|
||||
- 검색: `terminal_first_render_empty_shell`, `empty_shell`, `__empty__` literal
|
||||
|
||||
**정책 결정 (lock — universal, mdx 별 X)**:
|
||||
|
||||
| condition | route |
|
||||
|---|---|
|
||||
| `auto_renderable` 1+ | rank-1 으로 직접 렌더 |
|
||||
| `auto_renderable` 0 + `ai_adaptation_required` 1+ | top 후보 AI adaptation route. selection_path = `"ai_adaptation_required"`, frame_template_id = 실제 frame |
|
||||
| `auto_renderable` 0 + `ai_adaptation_required` 0 (raw 후보 0 — C3) | **generic_fallback frame + AI adaptation** (mandatory, no longer deferred) |
|
||||
|
||||
**generic_fallback frame 정책**:
|
||||
- catalog 의 가장 universal frame 1 개 지정 (예: simple bullet + title 형)
|
||||
- **신설 frame X — 기존 catalog 활용**
|
||||
- hardcode mdx 별 dispatch X — "raw 후보 0 일 때" 통일 트리거
|
||||
- selection_path = `"generic_fallback"`, candidate_status = `"ai_adaptation_required"` (AI 가 원문 재배치)
|
||||
- catalog_gap metric 기록 (운영 시그널 — terminal X)
|
||||
|
||||
**무엇이 더 이상 존재하지 않는가**:
|
||||
- `merge_type: "empty_shell"`
|
||||
- `selection_path: "empty_shell"`
|
||||
- `frame_template_id: "__empty__"`
|
||||
- `imp30_u4: "terminal_first_render_empty_shell"` (이게 terminal 이라는 의미)
|
||||
|
||||
**기록은 유지** (운영 시그널):
|
||||
- `candidate_status_summary` 같은 metric 으로 catalog_gap 비율 추적 — 단 terminal 아님
|
||||
|
||||
### Priority 4 — AI fallback 호출 trace (5~10분, read + verify)
|
||||
|
||||
**목적**: .env 가 `AI_FALLBACK_ENABLED=true` 인데 실제 호출되는지 확인. empty_shell 전에 끊기는지.
|
||||
|
||||
**파일**:
|
||||
- `src/phase_z2_ai_fallback/router.py:67` (`if not settings.ai_fallback_enabled:` gate)
|
||||
- `src/phase_z2_pipeline.py:4914` (`if not _settings.ai_fallback_enabled:`)
|
||||
|
||||
**검증**:
|
||||
- mdx 04/05 로컬 run 후 `step12_ai_repair.json` 확인 — AI 호출 attempt 있나?
|
||||
- 없으면 Priority 3 fix 후 다시 확인
|
||||
|
||||
### Priority 5 — Frontend candidate panel 검증 (5~10분, read + edit)
|
||||
|
||||
**목적**: backend 후보 살아도 frontend 숨기면 사용자 못 씀. reject / restructure / needs_adaptation 후보가 우측 panel 에 나와야.
|
||||
|
||||
**파일**:
|
||||
- `Front/client/src/components/FramePanel.tsx` (candidate rendering 로직)
|
||||
- `Front/client/src/components/SlideCanvas.tsx` (candidate selection 흐름)
|
||||
- API: `/api/run` response 의 candidate 필드 확인
|
||||
|
||||
**검증**:
|
||||
- mdx 04/05 로컬 run 결과 → frontend 가 reject 후보를 표시 / 선택 가능한지
|
||||
|
||||
### Quick fix (5분) — PHASE_Z_ALLOW_REJECT wire-up 또는 retire
|
||||
|
||||
**파일**:
|
||||
- `Front/vite.config.ts:903-904` — `PHASE_Z_ALLOW_REJECT: "1"` / `PHASE_Z_ALLOW_RESTRUCTURE: "1"`
|
||||
- `src/` — 현재 read 0 매치
|
||||
|
||||
**결정 안**:
|
||||
- **A. Wire-up**: `lookup_v4_candidates` 의 `include_reject` 가 이 env 도 source 로 read
|
||||
- **B. Retire**: vite spawn env 에서 제거 (dead var 제거)
|
||||
|
||||
선택은 작업 진행 중 결정.
|
||||
|
||||
---
|
||||
|
||||
## 5. 검증 — Acceptance criteria
|
||||
|
||||
### 각 priority 완료 후 sanity
|
||||
|
||||
| priority | local run | 기대 |
|
||||
|---|---|---|
|
||||
| 1 | (read-only) | ✅ done — 04/05 raw 후보 분포 + C1/C2/C3 root cause 식별 |
|
||||
| 2 | mdx04/05 local pipeline | step05_v4_evidence 의 각 candidate 에 `candidate_status` field 존재 (auto / ai_adaptation / blocked), reject 후보 surface — **C1 처리** |
|
||||
| 2.5 | mdx02/04 local pipeline | `04-2` lookup → child `04-2.1` + `04-2.2` 후보 5 개 merge 됨 (debug log 에 origin child_id 보존), `02-2` 도 동일 — **C2 처리** |
|
||||
| 3 | mdx04/05 local pipeline | step06_composition_plan 의 `selection_path` ∈ {`auto_renderable_rank_1`, `ai_adaptation_required`, `generic_fallback`}, **`"empty_shell"` 0 회**, `frame_template_id != "__empty__"`, mdx 05 도 generic_fallback 으로 슬라이드 — **C1/C3 처리** |
|
||||
| 4 | mdx04/05 local pipeline | step12_ai_repair.json 에 AI 호출 trace 존재 (ai_adaptation route 도달) |
|
||||
| 5 | frontend 새로고침 | mdx 04/05 의 우측 panel 에 후보 surface + status 별 시각 구분 (예: 배지) |
|
||||
|
||||
### 최종 demo readiness
|
||||
|
||||
- [ ] mdx 01: 의미 있는 슬라이드 나옴
|
||||
- [ ] mdx 02: 의미 있는 슬라이드 나옴 (02-2 = C2 alignment 검증)
|
||||
- [ ] mdx 03: 정상 (회귀 X 검증)
|
||||
- [ ] mdx 04: 04-1 (C1 reject→AI), 04-2 (C2 alignment) 모두 슬라이드 나옴
|
||||
- [ ] mdx 05: 05-1 + 05-2 generic_fallback 으로 슬라이드 나옴 (C3 처리)
|
||||
- [ ] **5 mdx 통합: empty_shell 결과 = 0** (어떤 mdx 도 empty 안 됨 — 정책 lock 검증)
|
||||
- [ ] **C2 검증**: 04-2 가 04-2.1/04-2.2 child candidates 받아옴 (parent ↔ child alignment universal)
|
||||
- [ ] **C3 검증**: mdx 05 가 V4 source 없음에도 generic_fallback frame + AI adaptation 으로 슬라이드 (raw 후보 0 일반 정책)
|
||||
- [ ] 각 슬라이드 frame 변경 가능 (panel UI, status 분류 surface)
|
||||
- [ ] AI 적용/재배치 동작 (mdx 텍스트 보존)
|
||||
- [ ] reject-only mdx (mdx 04-1, mdx 02-2.1) 가 AI adaptation 으로 슬라이드 생성 — 정책 lock end-to-end 검증
|
||||
|
||||
---
|
||||
|
||||
## 6. 수정 대상 파일 — 최종 inventory
|
||||
|
||||
**Edit 대상**:
|
||||
- `src/phase_z2_pipeline.py` — lookup_v4 함수 + Step 5/9 caller + empty_shell 분기
|
||||
- `src/phase_z2_composition.py` — empty_shell terminal disposition 우회 + reject 후보 routing
|
||||
- (조건부) `src/phase_z2_ai_fallback/router.py` — gate 확인 후 필요 시
|
||||
- (조건부) `Front/vite.config.ts` — `PHASE_Z_ALLOW_REJECT` wire-up 또는 retire
|
||||
- (조건부) `Front/client/src/components/FramePanel.tsx` — reject 후보 surface 검증/추가
|
||||
|
||||
**Read-only 진단**:
|
||||
- `tests/matching/v4_full32_result.yaml`
|
||||
- `data/runs/mdx04_clean_check_v2/phase_z2/steps/step05_v4_evidence.json`
|
||||
- `data/runs/mdx04_clean_check_v2/phase_z2/steps/step06_composition_plan.json`
|
||||
- `data/runs/mdx05_clean_check_v2/phase_z2/steps/step05_v4_evidence.json`
|
||||
- `data/runs/mdx05_clean_check_v2/phase_z2/steps/step06_composition_plan.json`
|
||||
- `data/runs/mdx05_clean_check_v2/phase_z2/steps/step12_ai_repair.json`
|
||||
- (priority 4 진행 후) 새 run 의 동일 step files
|
||||
|
||||
**MDX 원문 (절대 변경 X)**:
|
||||
- `samples/mdx_batch/01.mdx` ~ `05.mdx`
|
||||
|
||||
---
|
||||
|
||||
## 7. 진행 추적
|
||||
|
||||
각 priority 완료 후 이 섹션에 결과 한 줄 추가 — 작업 끝나면 정리해서 후속 Gitea issue / commit 본문 reference.
|
||||
|
||||
- [x] P1: Raw 후보 분포 확인 — C1 (02-2.1, 04-1 all-reject) / C2 (04-2 ↔ 04-2.1/04-2.2 split) / C3 (mdx 05 source 누락) 식별
|
||||
- [ ] P2: V4 후보 보존 + 3-status 분류 (C1 처리 — reject = ai_adaptation_required)
|
||||
- [ ] P2.5: Section key alignment policy (C2 처리 — child → parent pool merge, universal)
|
||||
- [ ] P3: empty_shell terminal 제거 + AI adaptation + generic_fallback frame mandatory (C1/C3 처리)
|
||||
- [ ] P4: AI fallback 호출 trace
|
||||
- [ ] P5: Frontend candidate panel 검증 (status surface)
|
||||
- [ ] Quick fix: PHASE_Z_ALLOW_REJECT wire-up 또는 retire
|
||||
|
||||
---
|
||||
|
||||
## 8. 후속 정리 (이 세션 후)
|
||||
|
||||
작업 안정화 되면 별도 task 로:
|
||||
- 새 Gitea issue 생성 (P1~P5 결과 evidence 기반)
|
||||
- 분리된 commits 으로 push
|
||||
- 23 suspect pool deep-dive (별 axis)
|
||||
- queue tracker #97 update
|
||||
|
||||
이 단계는 **이번 세션 scope 외**. 데모 우선.
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: 건설산업 DX의 올바른 이해
|
||||
sidebar:
|
||||
order: 00
|
||||
---
|
||||
|
||||
* **용어의 혼용**
|
||||
|
||||
* 건설산업의 디지털 전환 논의에서 DX(Digital Transformation)와 BIM(Building Information Modeling)이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있음
|
||||
* 이로인해 BIM기술의 도입을 DX의 완성으로 오인하거나, DX를 BIM 기술 도입 수준으로 한정하는 인식 확산
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>혼용 대표 사례</summary>
|
||||
|
||||
<div style={{marginTop: '10px', paddingLeft: '15px', borderLeft: '3px solid #ddd', fontSize: '0.9rem', color: '#666'}}>
|
||||
* **[스마트 건설 활성화 방안(2022.07)]**
|
||||
* 추진과제 : 건설산업 디지털화
|
||||
* 실행과제 : BIM 전면 도입, BIM 전문인력 양성
|
||||
* **[제7차 건설기술진흥 기본계획(2023.12)]**
|
||||
* 추진방향 : 디지털 전환을 통한 스마트 건설 확산
|
||||
* 추진과제 : BIM 도입으로 건설산업 디지털화
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
* 건설산업의 DX를 올바르게 이해하기 위해 각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
|
||||
## 1. 용어 정의
|
||||
|
||||
<br/>
|
||||
|
||||
* **건설산업**
|
||||
* 다양한 시설물을 각 산업마다의 광범위한 기술을 통합 및 융합하여 만들어내는 종합산업
|
||||
* 목적 시설물의 품질 욕구를 충족시키면서 최단기간내에 최소 비용으로 편리하고 안전하며 우수한 성능의 시설물 완성을 목표로 함
|
||||
|
||||
<br/>
|
||||
|
||||
* **BIM(Building Information Modeling) : 디지털 전환을 위한 핵심 기술**
|
||||
* 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구
|
||||
* 건설 정보와 절차를 표준화된 방식으로 연계하고 디지털 협업이 가능하도록 하는 핵심 인프라 기술
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*건설산업 BIM 기본지침, 국토교통부, 2020*
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
* **DX(Digital Transformation) : 산업 패러다임의 변화**
|
||||
* 디지털 기술을 기반으로 산업 전반의 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
|
||||
* 단순한 기술 도입이 아닌, 고객 가치와 의사결정 방식의 근본적인 변화로 산업의 새로운 방향을 정립하는 것을 의미함
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*Digital Transformation, IBM Institute for Business Value, 2011 / What is Digital Transformation?, Agile Elephant, 2015*
|
||||
</div>
|
||||
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 2. 용어간 상호관계
|
||||
|
||||
* DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 프로세스를 혁신하는 상위개념
|
||||
* 건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 기술융합을 통해서만 실현 또는 구현 가능
|
||||
* GIS의 역할 : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공
|
||||
* BIM의 역할 : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공
|
||||

|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*[그림 1] DX와 핵심기술간 상호관계*
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>DX와 BIM의 구분</summary>
|
||||
|
||||
<div style={{marginTop: '10px', paddingLeft: '15px', borderLeft: '3px solid #ddd', fontSize: '0.9rem', color: '#666'}}>
|
||||
| DX | 구분 | BIM |
|
||||
| :--- | :---: | ---: |
|
||||
| **BIM << DX**<br/>(Engineering + Management 통합) | **범위** | **Only 3D**<br/>(형상 구현 중심) |
|
||||
| **제작 및 운영**(상용 + 전용 40~80개)<br/>[Rhino, Sketchup, Blender..] + [EG-BIM 등] | **S/W** | **모델 제작용 상용 SW**<br/>[Revit, Civil 3D, Navisworks, Autocad] |
|
||||
| **근본적 문제의식을 통한 개선** | **프로세스** | **기존 2D 설계 방식 유지** |
|
||||
| **공학 정보 및 콘텐츠 연계에 집중**<br/>**도면, 수량, 시공계획 등 일식** | **성과품** | **3D 모델 중심**<br/>**기존 성과품 유지** |
|
||||
| **설계/시공 생산성 혁신**(개념의 재정립) | **활용** | **3D 모델에 의한 일반적 이해 향상** |
|
||||
| **전 생애주기 활용 시스템** | **확장성** | **(설계/시공/운영) 분야별 단절** |
|
||||
| **구체화(복잡) - 적극적/구체적 실현 방안** | **수행 개념** | **단순화(오류) - 수동적/집단적 동질화** |
|
||||
| **적극적, 주체적인 기술 접목/융합** | **CIVIL + IT** | **소극적, 상용 기술에 의존** |
|
||||
| **자체 수행 능력 - 지속가능성 확보** | **주체** | **S/W 제작사 판매 정책에 의존** |
|
||||
| **차별화 및 경쟁력 확보, 해외 진출** | **발주처** | **평준화, 국내 중심** |
|
||||
| **IT + CIVIL ENG 220명 운영 + 기술 개발** | **설계사** | **소규모 BIM팀 운영 + 단순교육에 집중** |
|
||||
| **분야 확장 모델 및 시스템** | **시공사** | **국내 토목 소극적/해외 토목증가** |
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 **가장 기초가 되는 일부분**이다
|
||||
:::
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: DX의 시행 목표 및 기대효과
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import DxEffect from '../../../../components/dx.astro';
|
||||
|
||||
|
||||
## 1. DX의 궁극적 목표
|
||||
|
||||
- **안전과 품질**
|
||||
- 시설물의 요구 성능을 설계-시공-운영 전 과정에서 **디지털로 검증**하여 **안전성 확보**
|
||||
- Copy & Paste로 하향 평준화된 성과물의 **하자 최소화**로 **고품질 성과물 제공**
|
||||
<br/>
|
||||
- **생산성 향상**
|
||||
- Analogue 기반 업무를 Digital 기반 프로세스로 전환하여 **업무 속도·정확성·일관성 향상**
|
||||
- 건설 비용 및 유지관리비 절감, 건설 기간 단축, 인력투입 최소화를 통해 **부가가치 제고**
|
||||
<br/>
|
||||
- **소통과 신뢰**
|
||||
- 성과품과 Solution을 통한 협업 강화로 **의사소통 효율 및 운영·유지관리**의 **편리성 증진**
|
||||
- 3D 모델 및 데이터 기반 검증을 통한 **오류 최소화 및 Claim 예방**으로 **신뢰성 확보**
|
||||

|
||||
<br/>
|
||||
|
||||
## 2. DX 기반 Process 혁신에 따른 주체별 기대효과
|
||||
<br/>
|
||||
### 2.1 업무 수행 과정(Process)의 변화
|
||||
- **생산 방식**: 수작업 의존의 반복 업무에서 벗어나, **SW를 활용한 체계화된 방식**으로 전환
|
||||
- **인지·검토**: 2D 도면 해석 중심에서 **3D 모델 기반의 직관적 인지·검토 체계**로 전환
|
||||
- **협업 구조**: 개별 문서 중심 협업에서 **데이터 통합 기반의 정보 공유·관리 협업 환경**으로 전환
|
||||
- **검증·대응**: 사후 대응 중심의 문제 처리에서 **사전 검증 중심의 예방적 업무 방식**으로 전환
|
||||
<br/>
|
||||
### 2.2 DX 시행 주체별 기대효과
|
||||
|
||||
<DxEffect />
|
||||
<br/>
|
||||
<br/>
|
||||
:::note[핵심 요약]
|
||||
* 고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다.
|
||||
:::
|
||||
<br/>
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
title: DX 지연 요인
|
||||
sidebar:
|
||||
order: 03
|
||||
---
|
||||
|
||||
## 1. DX에 대한 인식
|
||||
<br/>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', justifycontent: 'center' }}>
|
||||
|
||||
<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)',
|
||||
position: 'relative',
|
||||
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 #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/>
|
||||
## 2. DX 추진의 실태
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 정책 및 발주 체계
|
||||
- **실질적 기술 경쟁을 저해하는 정책 집행**
|
||||
- 모든 설계사가 수행 능력을 갖추었다는 전제하에 정책 시행
|
||||
- 수행 능력이 없는 업체 선정 후 성과품의 수준을 낮추어 시행
|
||||
<br/>
|
||||
- **적용 효과가 있는 사례도 없이 방침부터 도입**
|
||||
- DX/BIM 적용에 따른 실무적 이득이 있다고 판단된 사례 부족
|
||||
- BIM 지침/방침 등을 시행 경험과 효과 검증도 없이 남발
|
||||
<br/>
|
||||
- **엔지니어링 S/W에 대한 개념 부재**
|
||||
- 다양한 엔지니어링 S/W의 특성에 대한 깊은 이해 없이 범용 S/W 선택
|
||||
- 대형 Global S/W 회사에 과도한 의존과 이에 예속되는 방침 남발로 전용 S/W 소멸
|
||||
<br/>
|
||||
- **기술투자(R&D) 없는 성과 창출 기대**
|
||||
- 단순 BIM S/W만 구입하면 될 것이라는 안일한 생각
|
||||
- 실질적 기술 개발 투자 노력 없이 남들이 하는 대로 하면 된다는 착각
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>발주처 반응</summary>
|
||||
<img src="/assets/images/발주처반응.png" />
|
||||
</details>
|
||||
|
||||
### 2.2 조직 및 수행 역량
|
||||
- **공학적 개념 정립 부재**
|
||||
- DX와 BIM의 차이점을 명확히 구분하지 못하고 접근 방식과 기술적 도구 사이의 혼란만 가중
|
||||
- 단순히 기술적 도구의 사용에 초점을 맞추느라 3D모델 제작 S/W에 과도하게 의존
|
||||
<br/>
|
||||
- **‘본업 기술력 확보’ 우선의 개념 부재**
|
||||
- 고도의 전문지식과 현장 경험이 축적된 Manual의 중요성과 필요성에 대한 이해 부족
|
||||
- 국가·발주처의 지침·방침에만 의존한 업체의 기술력
|
||||
<br/>
|
||||
- **DX/BIM의 근본 취지와 목표의 이해 부족**
|
||||
- DX에 의한 과정의 혁신과 결과물의 변화에 대한 고민 부재
|
||||
- 기술자가 직접 3D모델을 만들고 수정하며 설계를 수행하지 않고, 별도로 외주 처리하여 본질 회피
|
||||
<br/>
|
||||
- **과거의 타성에 머무르고 있는 기술자 집단**
|
||||
- 설계/감리/시공 임직원들의 Digital 무지와 전략적 무지
|
||||
- 교육과 학습을 통한 인재 양성보다 당장 실무 활용이 가능한 타사 인력 빼오기에 집중
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>설계·시공업계 반응</summary>
|
||||
<img src="/assets/images/설계시공업계반응.png" />
|
||||
</details>
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족은 DX 지연을 반복시키고 있다.
|
||||
:::
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 설계 방식의 왜곡
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
## 1. 설계의 자동화
|
||||
|
||||
- **설계의 개념**
|
||||
- **개념설계**: 건설사업의 타당성 조사, 환경영향평가 등을 수행하는 목적물과 방향성을 수립하는 단계
|
||||
- **실시설계**: 공학적 지식을 바탕으로 건설에 필요한 목적물의 형상과 규격, 공사비를 결정하는 단계
|
||||
- **시공상세설계**: 시공에 필요한 정보를 반영하여 목적물과 공정을 단계별로 구체화하는 단계
|
||||
|
||||
<br/>
|
||||
|
||||
- **설계의 특성**
|
||||
- 계획 단계부터 엔지니어의 경험과 공학적 판단에 기반한 의사결정이 필수적인 과정
|
||||
- 사업마다 고유한 환경 및 지역 조건이 다르므로 매번 일회성 작업으로 수행
|
||||
- 평면 및 종단 등 선형의 일부 변경만으로도 시설물 전체의 구조적 해석이 변화
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
- **'설계 자동화'라는 용어의 오용과 모순**
|
||||
- **기능적 대체의 오류**: '설계 자동화' S/W는 설계 전체 프로세스가 아닌, 단순 반복적인 제도(Drafting)나 수량 산출 등 단순 기능만을 대체하는 수준
|
||||
- **인식의 왜곡**: '자동화'라는 용어의 남용으로 엔지니어의 업무가 단순 반복 작업인 것으로 저평가되고 있으며, 가장 중요한 전문 지식과 경험 기반의 의사결정 단계가 경시
|
||||
- **완전 자동화의 불가능**: 설계는 각 단계가 유기적으로 연결된 지식 집약적 활동이므로, 엔지니어의 판단을 배제한 전체 프로세스의 완전 자동화는 불가능
|
||||
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
## 2. S/W 중심 설계 방식
|
||||
|
||||
### 1 기능·구조적 한계
|
||||
- 시간 단축과 오류 감소 등 '도구적 효율성'에만 집중하여, 설계 품질 향상이나 고부가가치를 창출하는 핵심 기술로 발전하지 못함
|
||||
- 대부분의 S/W가 2D 기반의 설계에 머물러 있어, 엔지니어의 입체적인 아이디어와 복잡한 현장 <br/>특수성을 충분히 표현하지 못함
|
||||
|
||||
### 2 기술 역량의 왜곡
|
||||
- S/W 개발사: 기술 혁신보다 기존 방식의 반복과 성과물 생산에만 치중하며, 수익성 중심의 개발 지속
|
||||
- 기업 및 기술자: 기존 방식의 한계를 분석하고 새로운 부가가치를 찾기보다는, 단순 S/W 활용 능력을 엔지니어의 핵심 역량으로 오인
|
||||
- 종속성 심화: 시간이 흐를수록 기술자는 S/W에 예속된 기능인으로 전락하고, 산업 전체는 저부가가치의 굴레를 벗어나지 못함
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 잘못된 자동화 인식과 도구 중심의 설계는 기술자를 S/W에 의존하는 기능인으로 전락시키며, <br/>설계의 본질과 경쟁력을 동시에 훼손한다.
|
||||
:::
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 설계 방식의 왜곡
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
## 1. 설계의 자동화
|
||||
|
||||
- **설계의 개념**
|
||||
- **개념설계**: 건설사업의 타당성 조사, 환경영향평가 등을 수행하는 목적물과 방향성을 수립하는 단계
|
||||
- **실시설계**: 공학적 지식을 바탕으로 건설에 필요한 목적물의 형상과 규격, 공사비를 결정하는 단계
|
||||
- **시공상세설계**: 시공에 필요한 정보를 반영하여 목적물과 공정을 단계별로 구체화하는 단계
|
||||
|
||||
<br/>
|
||||
|
||||
- **설계의 특성**
|
||||
- 계획 단계부터 엔지니어의 경험과 공학적 판단에 기반한 의사결정이 필수적인 과정
|
||||
- 사업마다 고유한 환경 및 지역 조건이 다르므로 매번 일회성 작업으로 수행
|
||||
- 평면 및 종단 등 선형의 일부 변경만으로도 시설물 전체의 구조적 해석이 변화
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
- **'설계 자동화'라는 용어의 오용과 모순**
|
||||
- **기능적 대체의 오류**: '설계 자동화' S/W는 설계 전체 프로세스가 아닌, 단순 반복적인 제도(Drafting)나 수량 산출 등 단순 기능만을 대체하는 수준
|
||||
- **인식의 왜곡**: '자동화'라는 용어의 남용으로 엔지니어의 업무가 단순 반복 작업인 것으로 저평가되고 있으며, 가장 중요한 전문 지식과 경험 기반의 의사결정 단계가 경시
|
||||
- **완전 자동화의 불가능**: 설계는 각 단계가 유기적으로 연결된 지식 집약적 활동이므로, 엔지니어의 판단을 배제한 전체 프로세스의 완전 자동화는 불가능
|
||||
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
## 2. S/W 중심 설계 방식
|
||||
|
||||
### 1 기능·구조적 한계
|
||||
- 시간 단축과 오류 감소 등 '도구적 효율성'에만 집중하여, 설계 품질 향상이나 고부가가치를 창출하는 핵심 기술로 발전하지 못함
|
||||
- 대부분의 S/W가 2D 기반의 설계에 머물러 있어, 엔지니어의 입체적인 아이디어와 복잡한 현장 <br/>특수성을 충분히 표현하지 못함
|
||||
|
||||
### 2 기술 역량의 왜곡
|
||||
- S/W 개발사: 기술 혁신보다 기존 방식의 반복과 성과물 생산에만 치중하며, 수익성 중심의 개발 지속
|
||||
- 기업 및 기술자: 기존 방식의 한계를 분석하고 새로운 부가가치를 찾기보다는, 단순 S/W 활용 능력을 엔지니어의 핵심 역량으로 오인
|
||||
- 종속성 심화: 시간이 흐를수록 기술자는 S/W에 예속된 기능인으로 전락하고, 산업 전체는 저부가가치의 굴레를 벗어나지 못함
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 잘못된 자동화 인식과 도구 중심의 설계는 기술자를 S/W에 의존하는 기능인으로 전락시키며, <br/>설계의 본질과 경쟁력을 동시에 훼손한다.
|
||||
:::
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 설계 방식의 왜곡
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
## 1. 설계의 자동화
|
||||
|
||||
- **설계의 개념**
|
||||
- **개념설계**: 건설사업의 타당성 조사, 환경영향평가 등을 수행하는 목적물과 방향성을 수립하는 단계
|
||||
- **실시설계**: 공학적 지식을 바탕으로 건설에 필요한 목적물의 형상과 규격, 공사비를 결정하는 단계
|
||||
- **시공상세설계**: 시공에 필요한 정보를 반영하여 목적물과 공정을 단계별로 구체화하는 단계
|
||||
|
||||
<br/>
|
||||
|
||||
- **설계의 특성**
|
||||
- 계획 단계부터 엔지니어의 경험과 공학적 판단에 기반한 의사결정이 필수적인 과정
|
||||
- 사업마다 고유한 환경 및 지역 조건이 다르므로 매번 일회성 작업으로 수행
|
||||
- 평면 및 종단 등 선형의 일부 변경만으로도 시설물 전체의 구조적 해석이 변화
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
- **'설계 자동화'라는 용어의 오용과 모순**
|
||||
- **기능적 대체의 오류**: '설계 자동화' S/W는 설계 전체 프로세스가 아닌, 단순 반복적인 제도(Drafting)나 수량 산출 등 단순 기능만을 대체하는 수준
|
||||
- **인식의 왜곡**: '자동화'라는 용어의 남용으로 엔지니어의 업무가 단순 반복 작업인 것으로 저평가되고 있으며, 가장 중요한 전문 지식과 경험 기반의 의사결정 단계가 경시
|
||||
- **완전 자동화의 불가능**: 설계는 각 단계가 유기적으로 연결된 지식 집약적 활동이므로, 엔지니어의 판단을 배제한 전체 프로세스의 완전 자동화는 불가능
|
||||
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
## 2. S/W 중심 설계 방식
|
||||
|
||||
### 1 기능·구조적 한계
|
||||
- 시간 단축과 오류 감소 등 '도구적 효율성'에만 집중하여, 설계 품질 향상이나 고부가가치를 창출하는 핵심 기술로 발전하지 못함
|
||||
- 대부분의 S/W가 2D 기반의 설계에 머물러 있어, 엔지니어의 입체적인 아이디어와 복잡한 현장 <br/>특수성을 충분히 표현하지 못함
|
||||
|
||||
### 2 기술 역량의 왜곡
|
||||
- S/W 개발사: 기술 혁신보다 기존 방식의 반복과 성과물 생산에만 치중하며, 수익성 중심의 개발 지속
|
||||
- 기업 및 기술자: 기존 방식의 한계를 분석하고 새로운 부가가치를 찾기보다는, 단순 S/W 활용 능력을 엔지니어의 핵심 역량으로 오인
|
||||
- 종속성 심화: 시간이 흐를수록 기술자는 S/W에 예속된 기능인으로 전락하고, 산업 전체는 저부가가치의 굴레를 벗어나지 못함
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 잘못된 자동화 인식과 도구 중심의 설계는 기술자를 S/W에 의존하는 기능인으로 전락시키며, <br/>설계의 본질과 경쟁력을 동시에 훼손한다.
|
||||
:::
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: 건설산업 DX의 올바른 이해
|
||||
sidebar:
|
||||
order: 00
|
||||
---
|
||||
|
||||
* **용어의 혼용**
|
||||
|
||||
* 건설산업의 디지털 전환 논의에서 DX(Digital Transformation)와 BIM(Building Information Modeling)이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있음
|
||||
* 이로인해 BIM기술의 도입을 DX의 완성으로 오인하거나, DX를 BIM 기술 도입 수준으로 한정하는 인식 확산
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>혼용 대표 사례</summary>
|
||||
|
||||
<div style={{marginTop: '10px', paddingLeft: '15px', borderLeft: '3px solid #ddd', fontSize: '0.9rem', color: '#666'}}>
|
||||
* **[스마트 건설 활성화 방안(2022.07)]**
|
||||
* 추진과제 : 건설산업 디지털화
|
||||
* 실행과제 : BIM 전면 도입, BIM 전문인력 양성
|
||||
* **[제7차 건설기술진흥 기본계획(2023.12)]**
|
||||
* 추진방향 : 디지털 전환을 통한 스마트 건설 확산
|
||||
* 추진과제 : BIM 도입으로 건설산업 디지털화
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
* 건설산업의 DX를 올바르게 이해하기 위해 각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
|
||||
## 1. 용어 정의
|
||||
|
||||
<br/>
|
||||
|
||||
* **건설산업**
|
||||
* 다양한 시설물을 각 산업마다의 광범위한 기술을 통합 및 융합하여 만들어내는 종합산업
|
||||
* 목적 시설물의 품질 욕구를 충족시키면서 최단기간내에 최소 비용으로 편리하고 안전하며 우수한 성능의 시설물 완성을 목표로 함
|
||||
|
||||
<br/>
|
||||
|
||||
* **BIM(Building Information Modeling) : 디지털 전환을 위한 핵심 기술**
|
||||
* 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구
|
||||
* 건설 정보와 절차를 표준화된 방식으로 연계하고 디지털 협업이 가능하도록 하는 핵심 인프라 기술
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*건설산업 BIM 기본지침, 국토교통부, 2020*
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
* **DX(Digital Transformation) : 산업 패러다임의 변화**
|
||||
* 디지털 기술을 기반으로 산업 전반의 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
|
||||
* 단순한 기술 도입이 아닌, 고객 가치와 의사결정 방식의 근본적인 변화로 산업의 새로운 방향을 정립하는 것을 의미함
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*Digital Transformation, IBM Institute for Business Value, 2011 / What is Digital Transformation?, Agile Elephant, 2015*
|
||||
</div>
|
||||
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 2. 용어간 상호관계
|
||||
|
||||
* DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 프로세스를 혁신하는 상위개념
|
||||
* 건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 기술융합을 통해서만 실현 또는 구현 가능
|
||||
* GIS의 역할 : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공
|
||||
* BIM의 역할 : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공
|
||||

|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#999',
|
||||
marginTop: '5px',
|
||||
lineHeight: '1.4',
|
||||
paddingLeft: '0px' }}>
|
||||
*[그림 1] DX와 핵심기술간 상호관계*
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>DX와 BIM의 구분</summary>
|
||||
|
||||
<div style={{marginTop: '10px', paddingLeft: '15px', borderLeft: '3px solid #ddd', fontSize: '0.9rem', color: '#666'}}>
|
||||
| DX | 구분 | BIM |
|
||||
| :--- | :---: | ---: |
|
||||
| **BIM << DX**<br/>(Engineering + Management 통합) | **범위** | **Only 3D**<br/>(형상 구현 중심) |
|
||||
| **제작 및 운영**(상용 + 전용 40~80개)<br/>[Rhino, Sketchup, Blender..] + [EG-BIM 등] | **S/W** | **모델 제작용 상용 SW**<br/>[Revit, Civil 3D, Navisworks, Autocad] |
|
||||
| **근본적 문제의식을 통한 개선** | **프로세스** | **기존 2D 설계 방식 유지** |
|
||||
| **공학 정보 및 콘텐츠 연계에 집중**<br/>**도면, 수량, 시공계획 등 일식** | **성과품** | **3D 모델 중심**<br/>**기존 성과품 유지** |
|
||||
| **설계/시공 생산성 혁신**(개념의 재정립) | **활용** | **3D 모델에 의한 일반적 이해 향상** |
|
||||
| **전 생애주기 활용 시스템** | **확장성** | **(설계/시공/운영) 분야별 단절** |
|
||||
| **구체화(복잡) - 적극적/구체적 실현 방안** | **수행 개념** | **단순화(오류) - 수동적/집단적 동질화** |
|
||||
| **적극적, 주체적인 기술 접목/융합** | **CIVIL + IT** | **소극적, 상용 기술에 의존** |
|
||||
| **자체 수행 능력 - 지속가능성 확보** | **주체** | **S/W 제작사 판매 정책에 의존** |
|
||||
| **차별화 및 경쟁력 확보, 해외 진출** | **발주처** | **평준화, 국내 중심** |
|
||||
| **IT + CIVIL ENG 220명 운영 + 기술 개발** | **설계사** | **소규모 BIM팀 운영 + 단순교육에 집중** |
|
||||
| **분야 확장 모델 및 시스템** | **시공사** | **국내 토목 소극적/해외 토목증가** |
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<br/>
|
||||
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 **가장 기초가 되는 일부분**이다
|
||||
:::
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: DX의 시행 목표 및 기대효과
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import DxEffect from '../../../../components/dx.astro';
|
||||
|
||||
|
||||
## 1. DX의 궁극적 목표
|
||||
|
||||
- **안전과 품질**
|
||||
- 시설물의 요구 성능을 설계-시공-운영 전 과정에서 **디지털로 검증**하여 **안전성 확보**
|
||||
- Copy & Paste로 하향 평준화된 성과물의 **하자 최소화**로 **고품질 성과물 제공**
|
||||
<br/>
|
||||
- **생산성 향상**
|
||||
- Analogue 기반 업무를 Digital 기반 프로세스로 전환하여 **업무 속도·정확성·일관성 향상**
|
||||
- 건설 비용 및 유지관리비 절감, 건설 기간 단축, 인력투입 최소화를 통해 **부가가치 제고**
|
||||
<br/>
|
||||
- **소통과 신뢰**
|
||||
- 성과품과 Solution을 통한 협업 강화로 **의사소통 효율 및 운영·유지관리**의 **편리성 증진**
|
||||
- 3D 모델 및 데이터 기반 검증을 통한 **오류 최소화 및 Claim 예방**으로 **신뢰성 확보**
|
||||

|
||||
<br/>
|
||||
|
||||
## 2. DX 기반 Process 혁신에 따른 주체별 기대효과
|
||||
<br/>
|
||||
### 2.1 업무 수행 과정(Process)의 변화
|
||||
- **생산 방식**: 수작업 의존의 반복 업무에서 벗어나, **SW를 활용한 체계화된 방식**으로 전환
|
||||
- **인지·검토**: 2D 도면 해석 중심에서 **3D 모델 기반의 직관적 인지·검토 체계**로 전환
|
||||
- **협업 구조**: 개별 문서 중심 협업에서 **데이터 통합 기반의 정보 공유·관리 협업 환경**으로 전환
|
||||
- **검증·대응**: 사후 대응 중심의 문제 처리에서 **사전 검증 중심의 예방적 업무 방식**으로 전환
|
||||
<br/>
|
||||
### 2.2 DX 시행 주체별 기대효과
|
||||
|
||||
<DxEffect />
|
||||
<br/>
|
||||
<br/>
|
||||
:::note[핵심 요약]
|
||||
* 고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다.
|
||||
:::
|
||||
<br/>
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: DX 실행 체계 구축 방안
|
||||
sidebar:
|
||||
order: 02
|
||||
---
|
||||
|
||||
## 1. DX 시행을 위한 필수 요건
|
||||
|
||||
<br/>
|
||||
|
||||
* **기술(디지털)**
|
||||
* **Digital 기술(S/W, H/W)과 업무 Process의 통합**
|
||||
* 기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행
|
||||
* 프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율
|
||||
* **분야별 전문 지식(설계, 시공, 유지관리 등) 보유**
|
||||
* 건설 전 단계에 대한 근본적인 이해와 지식 및 경험
|
||||
* 최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식
|
||||
|
||||
<br/>
|
||||
|
||||
* **사람(역량)**
|
||||
* **혁신적 사고방식과 창의적 문제 해결 능력**
|
||||
* 기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피
|
||||
* 디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시
|
||||
* **사용자 중심 사고와 DX 수행 경험**
|
||||
* 사용자의 요구와 기대를 충족시키는 설계 및 구현
|
||||
* 시행착오를 포함한 수행 경험과 사용자 경험(UX)을 반영한 해결 방안 제시
|
||||
|
||||
<br/>
|
||||
|
||||
* **자연(여건)**
|
||||
* **지속적인 투자 및 실행 의지**
|
||||
* 기술 도입 초기 단계에 필요한 인력·기간·비용 등의 대규모 투자
|
||||
* 기술 고도화를 위한 지속적인 개선 및 투자 체계 구축
|
||||
* 변화와 혁신을 통해 부가가치를 창출하려는 실행 의지와 추진력
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
## 2. Process의 혁신과 Product의 변화
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 과정(Process)의 혁신
|
||||
|
||||
* **Analogue 기반 업무의 Digital화**
|
||||
|
||||
| As-is [Analogue] | 구분 | To-be [Digital] |
|
||||
| :--- | :---: | :--- |
|
||||
| **개념·문서·행정 절차 중심** | ➠ | **시각화된 목적물, 소통, 투명성 중심** |
|
||||
| **2D 도면, 전문가, 규정** | ➠ | **3D 모델, 참여자, 실체** |
|
||||
| **업무 구분(단절), 책임** | ➠ | **협업(융·복합), 창의성** |
|
||||
|
||||
<br/>
|
||||
|
||||
* **GIS + BIM의 연계**
|
||||
* 지리·지형·지반 등 위치정보(GIS)와 3D모델(형상, 속성정보) 기반의 건설 정보를 포함하는 BIM의 연계를 통한 업무 프로세스의 혁신
|
||||
|
||||
<br/>
|
||||
|
||||
* **사용자 중심의 Solution 제공**
|
||||
* 서로 다른 S/W로 작성되어 분절화된 Analogue 방식의 성과물과 정보물을 연계할 수 있는 설계·시공 Solution 제공
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
### 2.2 결과(Product)의 변화
|
||||
|
||||
* **Copy & Paste로 인해 하향 평준화된 기존 성과물의 품질 향상**
|
||||
* 과거 수작업으로 시행하면서 발생하던 오류 등의 최소화
|
||||
* 정확한 Data에 기반한 계획으로 고품질 성과물 도출
|
||||
|
||||
<br/>
|
||||
|
||||
* **Analogue 기반 도서 외 Digital 기반 정보물 추가**
|
||||
* 기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가
|
||||
|
||||
<br/>
|
||||
|
||||
* **Solution을 활용한 업무 효율화**
|
||||
* Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성
|
||||
* 건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* **DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 기대할 수 있다.**
|
||||
:::
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
title: DX 실행 체계 구축 방안
|
||||
sidebar:
|
||||
order: 02
|
||||
slide: /slides/03.html
|
||||
---
|
||||
|
||||
## 1. DX 시행을 위한 필수 요건
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="width: 100%; max-width: 800px; margin: 0 auto;">
|
||||
|
||||
<table style="width: 80%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 30px; font-family: 'Malgun Gothic', sans-serif; border-top: 1px solid #333; border-bottom: 1px solid #333;">
|
||||
<tr>
|
||||
<td rowspan="2" style="width: 40px; background: linear-gradient(to bottom, #0055aa, #003366); color: white; text-align: center; vertical-align: middle; padding: 10px; border: none; border-right: 1px dashed rgba(255, 255, 255, 0.5);">
|
||||
<div style="writing-mode: vertical-rl; font-weight: bold; font-size: 18px; letter-spacing: 2px; margin: 0 auto;">기술 (디지털)</div>
|
||||
</td>
|
||||
<td style="width: 45px; background-color: #0055aa; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; padding: 0;">技</td>
|
||||
<td style="padding: 15px; background-color: white; border: none; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">분야별 전문 지식(설계, 시공, 유지관리 등) 보유</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px;">
|
||||
<li>건설 전 단계에 대한 근본적인 이해와 지식 및 경험</li>
|
||||
<li>최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 45px; background-color: #003366; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; border-top: 1px dashed rgba(255,255,255,0.3); padding: 0;">術</td>
|
||||
<td style="padding: 15px 15px 15px 15px; background-color: white; border: none; border-top: 1px dotted #ccc; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">Digital 기술(S/W, H/W)과 업무 Process의 통합</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px; margin-bottom: 0;">
|
||||
<li>기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행</li>
|
||||
<li style="margin-bottom: 0;">프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="width: 80%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 30px; font-family: 'Malgun Gothic', sans-serif; border-top: 1px solid #333; border-bottom: 1px solid #333;">
|
||||
<tr>
|
||||
<td rowspan="2" style="width: 40px; background: linear-gradient(to bottom, #e67e22, #d35400); color: white; text-align: center; vertical-align: middle; padding: 10px; border: none; border-right: 1px dashed rgba(255, 255, 255, 0.5);">
|
||||
<div style="writing-mode: vertical-rl; font-weight: bold; font-size: 18px; letter-spacing: 2px; margin: 0 auto;">사람 (역량)</div>
|
||||
</td>
|
||||
<td style="width: 45px; background-color: #e67e22; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; padding: 0;">人</td>
|
||||
<td style="padding: 15px; background-color: white; border: none; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">혁신적 사고방식과 문제 해결 능력을 갖춘 기술자</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px;">
|
||||
<li>기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피</li>
|
||||
<li>디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 45px; background-color: #d35400; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; border-top: 1px dashed rgba(255,255,255,0.3); padding: 0;">材</td>
|
||||
<td style="padding: 15px 15px 15px 15px; background-color: white; border: none; border-top: 1px dotted #ccc; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">디지털화 역량 및 개발경험 많은 개발자</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px; margin-bottom: 0;">
|
||||
<li>다양한 Solution S/W를 개발할 수 있는 역량을 갖춘 Programmer </li>
|
||||
<li style="margin-bottom: 0;">사용자 중심의 생각과 시행 착오를 포함한 DX 수행 경험</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table style="width: 80%; table-layout: fixed; border-collapse: collapse; border-spacing: 0; margin-bottom: 30px; font-family: 'Malgun Gothic', sans-serif; border-top: 1px solid #333; border-bottom: 1px solid #333;">
|
||||
<tr>
|
||||
<td rowspan="2" style="width: 40px; background: linear-gradient(to bottom, #27ae60, #1e8449); color: white; text-align: center; vertical-align: middle; padding: 10px; border: none; border-right: 1px dashed rgba(255, 255, 255, 0.5);">
|
||||
<div style="writing-mode: vertical-rl; font-weight: bold; font-size: 18px; letter-spacing: 2px; margin: 0 auto;">자연 (여건)</div>
|
||||
</td>
|
||||
<td style="width: 45px; background-color: #27ae60; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; padding: 0;">天</td>
|
||||
<td style="padding: 15px; background-color: white; border: none; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">수용 가능한 사회 · 기업 · 제도 등의 여건</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px;">
|
||||
<li>목표만 제시하는 지침이 아닌, 수행사례와 효과검증 기반의 정책 시행</li>
|
||||
<li>단순 기술 도입이 아닌, 실제 활용 중심의 업무 환경 구축</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 45px; background-color: #1e8449; color: white; text-align: center; vertical-align: middle; font-size: 22px; font-weight: bold; border: none; border-top: 1px dashed rgba(255,255,255,0.3); padding: 0;">地</td>
|
||||
<td style="padding: 15px 15px 15px 15px; background-color: white; border: none; border-top: 1px dotted #ccc; vertical-align: top;">
|
||||
<div style="font-size: 17px; font-weight: bold; margin-bottom: 8px;">지속적인 투자 및 실행 의지</div>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #333; line-height: 1.5; font-size: 14px; margin-bottom: 0;">
|
||||
<li>기술 개발 및 고도화를 위한 대규모 투자와 지속적 개선</li>
|
||||
<li style="margin-bottom: 0;">부가가치 창출을 위한 실행 의지와 추진력</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
|
||||
## 2. Process의 혁신과 Product의 변화
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 과정(Process)의 혁신
|
||||
|
||||
* **Analogue 기반 업무의 Digital Transformation**
|
||||
|
||||
| As-is [Analogue] | 구분 | To-be [Digital] |
|
||||
| :--- | :---: | :--- |
|
||||
| 개념·문서·행정 절차 중심 | ➠ | 시각화된 목적물, 소통, 투명성 중심 |
|
||||
| 2D 도면, 전문가, 규정 | ➠ | 3D 모델, 참여자, 실체 |
|
||||
| 업무 구분(단절), 책임 | ➠ | 협업(융·복합), 창의성 |
|
||||
|
||||
<br/>
|
||||
|
||||
* **GIS + BIM의 연계**
|
||||
* GIS(지리·지형·지반 등 위치정보)와 BIM(3D 형상, 속성정보)의 연계를 통한 업무 프로세스의 혁신
|
||||
|
||||
<br/>
|
||||
|
||||
* **사용자 중심의 Solution 제공 (S/W)**
|
||||
* 서로 다른 S/W로 작성되어 분절화된 아날로그식 성과물과 정보물의 연계가 가능하도록 설계·시공 Solution 제공
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.2 결과(Product)의 변화
|
||||
|
||||
* **Copy & Paste로 인해 하향 평준화된 기존 성과물의 품질 향상**
|
||||
* 과거 수작업으로 시행하면서 발생하던 오류 등의 최소화
|
||||
* 정확한 Data에 기반한 계획으로 고품질 성과물 도출
|
||||
|
||||
<br/>
|
||||
|
||||
* **Analogue 기반 도서 외 Digital 기반 정보물 추가**
|
||||
* 기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가
|
||||
|
||||
<br/>
|
||||
|
||||
* **Solution을 활용한 업무 효율화**
|
||||
* Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성
|
||||
* 건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리
|
||||
|
||||
<br/>
|
||||

|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 기대할 수 있다.
|
||||
:::
|
||||
@@ -0,0 +1,262 @@
|
||||
---
|
||||
title: DX 지연 요인
|
||||
sidebar:
|
||||
order: 03
|
||||
---
|
||||
|
||||
## 1. DX에 대한 인식
|
||||
<br/>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px', justifycontent: 'center' }}>
|
||||
|
||||
<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)',
|
||||
position: 'relative',
|
||||
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 #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/>
|
||||
## 2. DX 추진의 실태
|
||||
|
||||
<br/>
|
||||
|
||||
### 2.1 정책 및 발주 체계
|
||||
- **실질적 기술 경쟁을 저해하는 정책 집행**
|
||||
- 모든 설계사가 수행 능력을 갖추었다는 전제하에 정책 시행
|
||||
- 수행 능력이 없는 업체 선정 후 성과품의 수준을 낮추어 시행
|
||||
<br/>
|
||||
- **적용 효과가 있는 사례도 없이 방침부터 도입**
|
||||
- DX/BIM 적용에 따른 실무적 이득이 있다고 판단된 사례 부족
|
||||
- BIM 지침/방침 등을 시행 경험과 효과 검증도 없이 남발
|
||||
<br/>
|
||||
- **엔지니어링 S/W에 대한 개념 부재**
|
||||
- 다양한 엔지니어링 S/W의 특성에 대한 깊은 이해 없이 범용 S/W 선택
|
||||
- 대형 Global S/W 회사에 과도한 의존과 이에 예속되는 방침 남발로 전용 S/W 소멸
|
||||
<br/>
|
||||
- **기술투자(R&D) 없는 성과 창출 기대**
|
||||
- 단순 BIM S/W만 구입하면 될 것이라는 안일한 생각
|
||||
- 실질적 기술 개발 투자 노력 없이 남들이 하는 대로 하면 된다는 착각
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>발주처 반응</summary>
|
||||
<img src="/assets/images/발주처반응.png" />
|
||||
</details>
|
||||
|
||||
### 2.2 조직 및 수행 역량
|
||||
- **공학적 개념 정립 부재**
|
||||
- DX와 BIM의 차이점을 명확히 구분하지 못하고 접근 방식과 기술적 도구 사이의 혼란만 가중
|
||||
- 단순히 기술적 도구의 사용에 초점을 맞추느라 3D모델 제작 S/W에 과도하게 의존
|
||||
<br/>
|
||||
- **‘본업 기술력 확보’ 우선의 개념 부재**
|
||||
- 고도의 전문지식과 현장 경험이 축적된 Manual의 중요성과 필요성에 대한 이해 부족
|
||||
- 국가·발주처의 지침·방침에만 의존한 업체의 기술력
|
||||
<br/>
|
||||
- **DX/BIM의 근본 취지와 목표의 이해 부족**
|
||||
- DX에 의한 과정의 혁신과 결과물의 변화에 대한 고민 부재
|
||||
- 기술자가 직접 3D모델을 만들고 수정하며 설계를 수행하지 않고, 별도로 외주 처리하여 본질 회피
|
||||
<br/>
|
||||
- **과거의 타성에 머무르고 있는 기술자 집단**
|
||||
- 설계/감리/시공 임직원들의 Digital 무지와 전략적 무지
|
||||
- 교육과 학습을 통한 인재 양성보다 당장 실무 활용이 가능한 타사 인력 빼오기에 집중
|
||||
<br/>
|
||||
<details>
|
||||
<summary style={{cursor: 'pointer', fontWeight: 'bold', color: '#555'}}>설계·시공업계 반응</summary>
|
||||
<img src="/assets/images/설계시공업계반응.png" />
|
||||
</details>
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족은 DX 지연을 반복시키고 있다.
|
||||
:::
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 설계 방식의 왜곡
|
||||
sidebar:
|
||||
order: 0
|
||||
---
|
||||
|
||||
## 1. 설계의 자동화
|
||||
|
||||
- **설계의 개념**
|
||||
- **개념설계**: 건설사업의 타당성 조사, 환경영향평가 등을 수행하는 목적물과 방향성을 수립하는 단계
|
||||
- **실시설계**: 공학적 지식을 바탕으로 건설에 필요한 목적물의 형상과 규격, 공사비를 결정하는 단계
|
||||
- **시공상세설계**: 시공에 필요한 정보를 반영하여 목적물과 공정을 단계별로 구체화하는 단계
|
||||
|
||||
<br/>
|
||||
|
||||
- **설계의 특성**
|
||||
- 계획 단계부터 엔지니어의 경험과 공학적 판단에 기반한 의사결정이 필수적인 과정
|
||||
- 사업마다 고유한 환경 및 지역 조건이 다르므로 매번 일회성 작업으로 수행
|
||||
- 평면 및 종단 등 선형의 일부 변경만으로도 시설물 전체의 구조적 해석이 변화
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
- **'설계 자동화'라는 용어의 오용과 모순**
|
||||
- **기능적 대체의 오류**: '설계 자동화' S/W는 설계 전체 프로세스가 아닌, 단순 반복적인 제도(Drafting)나 수량 산출 등 단순 기능만을 대체하는 수준
|
||||
- **인식의 왜곡**: '자동화'라는 용어의 남용으로 엔지니어의 업무가 단순 반복 작업인 것으로 저평가되고 있으며, 가장 중요한 전문 지식과 경험 기반의 의사결정 단계가 경시
|
||||
- **완전 자동화의 불가능**: 설계는 각 단계가 유기적으로 연결된 지식 집약적 활동이므로, 엔지니어의 판단을 배제한 전체 프로세스의 완전 자동화는 불가능
|
||||
|
||||
<br/>
|
||||
---
|
||||
<br/>
|
||||
## 2. S/W 중심 설계 방식
|
||||
|
||||
### 1 기능·구조적 한계
|
||||
- 시간 단축과 오류 감소 등 '도구적 효율성'에만 집중하여, 설계 품질 향상이나 고부가가치를 창출하는 핵심 기술로 발전하지 못함
|
||||
- 대부분의 S/W가 2D 기반의 설계에 머물러 있어, 엔지니어의 입체적인 아이디어와 복잡한 현장 <br/>특수성을 충분히 표현하지 못함
|
||||
|
||||
### 2 기술 역량의 왜곡
|
||||
- S/W 개발사: 기술 혁신보다 기존 방식의 반복과 성과물 생산에만 치중하며, 수익성 중심의 개발 지속
|
||||
- 기업 및 기술자: 기존 방식의 한계를 분석하고 새로운 부가가치를 찾기보다는, 단순 S/W 활용 능력을 엔지니어의 핵심 역량으로 오인
|
||||
- 종속성 심화: 시간이 흐를수록 기술자는 S/W에 예속된 기능인으로 전락하고, 산업 전체는 저부가가치의 굴레를 벗어나지 못함
|
||||
|
||||
<br/>
|
||||
---
|
||||
|
||||
:::note[핵심 요약]
|
||||
* 잘못된 자동화 인식과 도구 중심의 설계는 기술자를 S/W에 의존하는 기능인으로 전락시키며, <br/>설계의 본질과 경쟁력을 동시에 훼손한다.
|
||||
:::
|
||||
@@ -0,0 +1,150 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.phase_z2_mapper import _extract_bold_or_plain, split_source
|
||||
from src.phase_z2_pipeline import parse_mdx
|
||||
|
||||
MDX_ROOT = ROOT / "samples" / "mdx"
|
||||
|
||||
|
||||
CONTRACT: dict[str, dict[str, Any]] = {
|
||||
"01. 건설산업 DX의 올바른 이해(0127).mdx": {
|
||||
"top_sections": ["1. 용어 정의", "2. 용어간 상호관계"],
|
||||
"internal_labels": {
|
||||
"01-1": ["건설산업", "BIM(Building Information Modeling) : 디지털 전환을 위한 핵심 기술", "DX(Digital Transformation) : 산업 패러다임의 변화"],
|
||||
"01-2": ["DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 프로세스를 혁신하는 상위개념", "DX와 BIM의 구분"],
|
||||
},
|
||||
},
|
||||
"02. DX의 시행 목표 및 기대효과.mdx": {
|
||||
"top_sections": ["1. DX의 궁극적 목표", "2. DX 기반 Process 혁신에 따른 주체별 기대효과"],
|
||||
"h3": {
|
||||
"02-2": ["2.1 업무 수행 과정(Process)의 변화", "2.2 DX 시행 주체별 기대효과"],
|
||||
},
|
||||
"internal_labels": {
|
||||
"02-1": ["안전과 품질", "생산성 향상", "소통과 신뢰"],
|
||||
},
|
||||
},
|
||||
"03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx": {
|
||||
"top_sections": ["1. DX 시행을 위한 필수 요건", "2. Process의 혁신과 Product의 변화"],
|
||||
"h3": {
|
||||
"03-2": ["2.1 과정(Process)의 혁신", "2.2 결과(Product)의 변화"],
|
||||
},
|
||||
"internal_labels": {
|
||||
"03-1": ["기술(디지털)", "사람(역량)", "자연(여건)"],
|
||||
},
|
||||
},
|
||||
"04. DX 지연 요인.mdx": {
|
||||
"top_sections": ["1. DX에 대한 인식", "2. DX 추진의 실태"],
|
||||
"h3": {
|
||||
"04-1": ["기술 및 소프트웨어 이해도", "효과와 효율성", "인력 및 교육", "경제적 부담", "실무 및 적용성"],
|
||||
"04-2": ["2.1 정책 및 발주 체계", "2.2 조직 및 수행 역량"],
|
||||
},
|
||||
},
|
||||
"05. 설계 방식의 왜곡.mdx": {
|
||||
"top_sections": ["1. 설계의 자동화", "2. S/W 중심 설계 방식"],
|
||||
"h3": {
|
||||
"05-2": ["1 기능·구조적 한계", "2 기술 역량의 왜곡"],
|
||||
},
|
||||
"internal_labels": {
|
||||
"05-1": ["설계의 개념", "설계의 특성", "'설계 자동화'라는 용어의 오용과 모순"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _h3_titles(raw_content: str) -> list[str]:
|
||||
return [
|
||||
m.group(1).strip()
|
||||
for m in re.finditer(r"(?m)^###\s+(.+?)\s*$", raw_content or "")
|
||||
]
|
||||
|
||||
|
||||
def _top_bullet_labels(raw_content: str) -> list[str]:
|
||||
return [
|
||||
_extract_bold_or_plain(top_line)
|
||||
for top_line, _nested in split_source("top_bullets", raw_content or "")
|
||||
]
|
||||
|
||||
|
||||
def _contains_label(labels: list[str], expected: str) -> bool:
|
||||
return any(expected == label or expected in label for label in labels)
|
||||
|
||||
|
||||
def check_one(filename: str, spec: dict[str, Any]) -> dict[str, Any]:
|
||||
path = MDX_ROOT / filename
|
||||
title, sections, footer = parse_mdx(path)
|
||||
section_titles = [s.title for s in sections]
|
||||
errors: list[str] = []
|
||||
|
||||
expected_top = spec.get("top_sections", [])
|
||||
if section_titles != expected_top:
|
||||
errors.append(
|
||||
f"top_sections mismatch: expected={expected_top!r}, actual={section_titles!r}"
|
||||
)
|
||||
|
||||
section_reports = []
|
||||
for section in sections:
|
||||
h3_titles = _h3_titles(section.raw_content)
|
||||
bullet_labels = _top_bullet_labels(section.raw_content)
|
||||
expected_h3 = (spec.get("h3") or {}).get(section.section_id)
|
||||
if expected_h3 is not None and h3_titles != expected_h3:
|
||||
errors.append(
|
||||
f"{section.section_id} h3 mismatch: expected={expected_h3!r}, actual={h3_titles!r}"
|
||||
)
|
||||
expected_labels = (spec.get("internal_labels") or {}).get(section.section_id, [])
|
||||
missing_labels = [
|
||||
label for label in expected_labels if not _contains_label(bullet_labels, label)
|
||||
]
|
||||
if missing_labels:
|
||||
errors.append(
|
||||
f"{section.section_id} internal labels missing: {missing_labels!r}"
|
||||
)
|
||||
section_reports.append({
|
||||
"section_id": section.section_id,
|
||||
"title": section.title,
|
||||
"h3_titles": h3_titles,
|
||||
"top_bullet_labels_count": len(bullet_labels),
|
||||
"top_bullet_labels_sample": bullet_labels[:12],
|
||||
})
|
||||
|
||||
return {
|
||||
"filename": filename,
|
||||
"title": title,
|
||||
"has_footer": bool(footer),
|
||||
"passed": not errors,
|
||||
"errors": errors,
|
||||
"sections": section_reports,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--write-report", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
reports = [check_one(filename, spec) for filename, spec in CONTRACT.items()]
|
||||
result = {
|
||||
"passed": all(report["passed"] for report in reports),
|
||||
"reports": reports,
|
||||
}
|
||||
text = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
print(text)
|
||||
if args.write_report:
|
||||
args.write_report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.write_report.write_text(text + "\n", encoding="utf-8")
|
||||
return 0 if result["passed"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from src.mdx_text_atoms import compare_atom_sets, extract_text_atoms
|
||||
|
||||
|
||||
DEFAULT_PAIRS = [
|
||||
(
|
||||
"samples/mdx/01. 건설산업 DX의 올바른 이해(0127)(원본).mdx",
|
||||
"samples/mdx/01. 건설산업 DX의 올바른 이해(0127).mdx",
|
||||
),
|
||||
(
|
||||
"samples/mdx/02. DX의 시행 목표 및 기대효과(원본).mdx",
|
||||
"samples/mdx/02. DX의 시행 목표 및 기대효과.mdx",
|
||||
),
|
||||
(
|
||||
"samples/mdx/04. DX 지연 요인(원본).mdx",
|
||||
"samples/mdx/04. DX 지연 요인.mdx",
|
||||
),
|
||||
(
|
||||
"samples/mdx/05. 설계 방식의 왜곡(원본).mdx",
|
||||
"samples/mdx/05. 설계 방식의 왜곡.mdx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Compare original MDX text atoms with standardized MDX text atoms.")
|
||||
parser.add_argument("--root", default=".", help="Repository root. Defaults to current directory.")
|
||||
parser.add_argument("--max-items", type=int, default=8, help="Max examples per mismatch category.")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
any_failed = False
|
||||
for original_rel, standardized_rel in DEFAULT_PAIRS:
|
||||
original = root / original_rel
|
||||
standardized = root / standardized_rel
|
||||
print(f"=== {standardized.name} ===")
|
||||
if not original.exists():
|
||||
print(f" ERROR original missing: {original_rel}")
|
||||
any_failed = True
|
||||
continue
|
||||
if not standardized.exists():
|
||||
print(f" ERROR standardized missing: {standardized_rel}")
|
||||
any_failed = True
|
||||
continue
|
||||
|
||||
original_atoms = extract_text_atoms(original)
|
||||
standardized_atoms = extract_text_atoms(standardized)
|
||||
comparison = compare_atom_sets(original_atoms, standardized_atoms)
|
||||
missing = comparison["missing_from_standardized"]
|
||||
added = comparison["added_in_standardized"]
|
||||
print(f" original_atoms : {len(original_atoms)}")
|
||||
print(f" standardized_atoms : {len(standardized_atoms)}")
|
||||
print(f" missing : {len(missing)}")
|
||||
print(f" added : {len(added)}")
|
||||
if missing or added:
|
||||
any_failed = True
|
||||
_print_examples("missing_from_standardized", missing, args.max_items)
|
||||
_print_examples("added_in_standardized", added, args.max_items)
|
||||
else:
|
||||
print(" OK text atom parity")
|
||||
print()
|
||||
|
||||
return 1 if any_failed else 0
|
||||
|
||||
|
||||
def _print_examples(label: str, atoms, max_items: int) -> None:
|
||||
if not atoms:
|
||||
return
|
||||
print(f" {label} examples:")
|
||||
for atom in atoms[:max_items]:
|
||||
print(f" L{atom.line_no} {atom.kind}: {atom.normalized}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from html import unescape
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.mdx_text_atoms import compare_atom_sets, extract_text_atoms, normalize_text_atom
|
||||
|
||||
|
||||
def _compact(text: str) -> str:
|
||||
normalized = normalize_text_atom(text or "")
|
||||
return re.sub(r"[^0-9A-Za-z가-힣]+", "", normalized)
|
||||
|
||||
|
||||
def _strip_html_to_text(html: str) -> str:
|
||||
html = re.sub(r"(?is)<script.*?</script>", "\n", html)
|
||||
html = re.sub(r"(?is)<style.*?</style>", "\n", html)
|
||||
html = re.sub(r"(?is)<[^>]+>", "\n", html)
|
||||
return unescape(html)
|
||||
|
||||
|
||||
def _load_source_text(run_dir: Path) -> str:
|
||||
step02 = run_dir / "phase_z2" / "steps" / "step02_normalized.json"
|
||||
data = json.loads(step02.read_text(encoding="utf-8"))["data"]
|
||||
parts: list[str] = []
|
||||
if data.get("slide_title"):
|
||||
parts.append(str(data["slide_title"]))
|
||||
for section in data.get("sections") or []:
|
||||
if section.get("title"):
|
||||
parts.append(str(section["title"]))
|
||||
if section.get("raw_content"):
|
||||
parts.append(str(section["raw_content"]))
|
||||
if data.get("slide_footer"):
|
||||
parts.append(str(data["slide_footer"]))
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def check_run(run_dir: Path) -> dict:
|
||||
final_html = run_dir / "phase_z2" / "final.html"
|
||||
source_text = _load_source_text(run_dir)
|
||||
rendered_text = _strip_html_to_text(final_html.read_text(encoding="utf-8"))
|
||||
source_atoms = extract_text_atoms(source_text)
|
||||
rendered_atoms = extract_text_atoms(rendered_text)
|
||||
diff = compare_atom_sets(source_atoms, rendered_atoms)
|
||||
rendered_compact = _compact(" ".join(a.normalized for a in rendered_atoms))
|
||||
compact_missing = [
|
||||
atom for atom in source_atoms
|
||||
if _compact(atom.normalized) and _compact(atom.normalized) not in rendered_compact
|
||||
]
|
||||
return {
|
||||
"run_id": run_dir.name,
|
||||
"source_atoms": len(source_atoms),
|
||||
"rendered_atoms": len(rendered_atoms),
|
||||
"missing_count": len(compact_missing),
|
||||
"added_count": len(diff["added_in_standardized"]),
|
||||
"missing": compact_missing,
|
||||
"added": diff["added_in_standardized"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("run_ids", nargs="+")
|
||||
parser.add_argument("--runs-root", default="data/runs")
|
||||
parser.add_argument("--max-items", type=int, default=8)
|
||||
args = parser.parse_args()
|
||||
|
||||
ok = True
|
||||
for run_id in args.run_ids:
|
||||
result = check_run(Path(args.runs_root) / run_id)
|
||||
if result["missing_count"]:
|
||||
ok = False
|
||||
print(f"=== {result['run_id']} ===")
|
||||
print(f" source_atoms : {result['source_atoms']}")
|
||||
print(f" rendered_atoms : {result['rendered_atoms']}")
|
||||
print(f" missing : {result['missing_count']}")
|
||||
print(f" added : {result['added_count']}")
|
||||
for item in result["missing"][: args.max_items]:
|
||||
print(f" - missing: {item.text}")
|
||||
for item in result["added"][: args.max_items]:
|
||||
print(f" + added: {item.text}")
|
||||
print()
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _open_driver(width: int, height: int):
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
|
||||
options = Options()
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument(f"--window-size={width},{height}")
|
||||
|
||||
candidates = [
|
||||
PROJECT_ROOT / "chromedriver",
|
||||
PROJECT_ROOT / "chromedriver.exe",
|
||||
]
|
||||
last_err: Exception | None = None
|
||||
for path in candidates:
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
driver = webdriver.Chrome(service=Service(str(path)), options=options)
|
||||
break
|
||||
except Exception as exc: # pragma: no cover - environment dependent
|
||||
last_err = exc
|
||||
else:
|
||||
try:
|
||||
driver = webdriver.Chrome(options=options)
|
||||
except Exception as exc: # pragma: no cover - environment dependent
|
||||
raise RuntimeError(f"selenium init failed: {last_err or exc}") from exc
|
||||
|
||||
driver.execute_cdp_cmd(
|
||||
"Emulation.setDeviceMetricsOverride",
|
||||
{
|
||||
"width": width,
|
||||
"height": height,
|
||||
"deviceScaleFactor": 1,
|
||||
"mobile": False,
|
||||
},
|
||||
)
|
||||
return driver
|
||||
|
||||
|
||||
def check_final_html(path: Path, *, width: int, height: int, tolerance: int) -> dict:
|
||||
driver = _open_driver(width, height)
|
||||
try:
|
||||
driver.get(path.resolve().as_uri())
|
||||
result = driver.execute_script(
|
||||
r"""
|
||||
const de = document.documentElement;
|
||||
const body = document.body;
|
||||
const slide = document.querySelector('.slide');
|
||||
const slideRect = slide ? slide.getBoundingClientRect() : null;
|
||||
const measure = (el) => ({
|
||||
clientWidth: el.clientWidth,
|
||||
clientHeight: el.clientHeight,
|
||||
scrollWidth: el.scrollWidth,
|
||||
scrollHeight: el.scrollHeight,
|
||||
});
|
||||
return {
|
||||
viewport: {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
},
|
||||
documentElement: measure(de),
|
||||
body: measure(body),
|
||||
slide: slideRect ? {
|
||||
x: slideRect.x,
|
||||
y: slideRect.y,
|
||||
width: slideRect.width,
|
||||
height: slideRect.height,
|
||||
right: slideRect.right,
|
||||
bottom: slideRect.bottom,
|
||||
} : null,
|
||||
};
|
||||
"""
|
||||
)
|
||||
finally:
|
||||
driver.quit()
|
||||
|
||||
failures: list[str] = []
|
||||
vp = result["viewport"]
|
||||
doc = result["documentElement"]
|
||||
body = result["body"]
|
||||
slide = result.get("slide")
|
||||
|
||||
if doc["scrollWidth"] > vp["innerWidth"] + tolerance:
|
||||
failures.append(
|
||||
f"document horizontal scroll: {doc['scrollWidth']} > {vp['innerWidth']}"
|
||||
)
|
||||
if doc["scrollHeight"] > vp["innerHeight"] + tolerance:
|
||||
failures.append(
|
||||
f"document vertical scroll: {doc['scrollHeight']} > {vp['innerHeight']}"
|
||||
)
|
||||
if body["scrollWidth"] > vp["innerWidth"] + tolerance:
|
||||
failures.append(f"body horizontal scroll: {body['scrollWidth']} > {vp['innerWidth']}")
|
||||
if body["scrollHeight"] > vp["innerHeight"] + tolerance:
|
||||
failures.append(f"body vertical scroll: {body['scrollHeight']} > {vp['innerHeight']}")
|
||||
if not slide:
|
||||
failures.append(".slide not found")
|
||||
else:
|
||||
if abs(slide["width"] - width) > tolerance or abs(slide["height"] - height) > tolerance:
|
||||
failures.append(
|
||||
f"slide size is {slide['width']}x{slide['height']}, expected {width}x{height}"
|
||||
)
|
||||
if slide["x"] < -tolerance or slide["y"] < -tolerance:
|
||||
failures.append(f"slide origin out of viewport: x={slide['x']}, y={slide['y']}")
|
||||
if slide["right"] > vp["innerWidth"] + tolerance:
|
||||
failures.append(f"slide right edge exceeds viewport: {slide['right']} > {vp['innerWidth']}")
|
||||
if slide["bottom"] > vp["innerHeight"] + tolerance:
|
||||
failures.append(f"slide bottom edge exceeds viewport: {slide['bottom']} > {vp['innerHeight']}")
|
||||
|
||||
return {
|
||||
"path": str(path),
|
||||
"passed": not failures,
|
||||
"failures": failures,
|
||||
"metrics": result,
|
||||
}
|
||||
|
||||
|
||||
def _run_id_to_final_html(run_id: str) -> Path:
|
||||
return PROJECT_ROOT / "data" / "runs" / run_id / "phase_z2" / "final.html"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("run_ids", nargs="+")
|
||||
parser.add_argument("--width", type=int, default=1280)
|
||||
parser.add_argument("--height", type=int, default=720)
|
||||
parser.add_argument("--tolerance", type=int, default=1)
|
||||
parser.add_argument("--write-report", type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
reports = []
|
||||
exit_code = 0
|
||||
for run_id in args.run_ids:
|
||||
path = _run_id_to_final_html(run_id)
|
||||
if not path.is_file():
|
||||
report = {
|
||||
"path": str(path),
|
||||
"passed": False,
|
||||
"failures": ["final.html not found"],
|
||||
"metrics": None,
|
||||
}
|
||||
else:
|
||||
report = check_final_html(
|
||||
path,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
tolerance=args.tolerance,
|
||||
)
|
||||
reports.append({"run_id": run_id, **report})
|
||||
status = "PASS" if report["passed"] else "FAIL"
|
||||
print(f"{run_id}: {status}")
|
||||
for failure in report["failures"]:
|
||||
print(f" - {failure}")
|
||||
if not report["passed"]:
|
||||
exit_code = 1
|
||||
|
||||
if args.write_report:
|
||||
args.write_report.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.write_report.write_text(
|
||||
json.dumps({"reports": reports}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
token = os.environ.get("GITEA_TOKEN")
|
||||
url = "https://gitea.hmac.kr/api/v1/repos/Kyeongmin/C.E.L_Slide_test2/issues/35/comments"
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers={"Authorization": f"token {token}"},
|
||||
json={"body": "Codex Test Hello from CLI"},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
print(response.status_code)
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase Z run 산출물을 frontend prototype 의 client/public/data/ 로 정적 export.
|
||||
|
||||
Usage:
|
||||
python scripts/sync_phase_z_run_to_frontend.py [run_id]
|
||||
python scripts/sync_phase_z_run_to_frontend.py mdx03_f29_fix_check
|
||||
python scripts/sync_phase_z_run_to_frontend.py --list # 사용 가능한 runs 목록
|
||||
|
||||
성격:
|
||||
- Static export (Phase 1 — 보고용 read-only viewer prototype).
|
||||
- 향후 Phase 2 에서 Express endpoint (server/index.ts) 로 대체 가능.
|
||||
- Frontend 의 designAgentApi.ts 의 loadRun(runId) 가 본 산출물을 fetch.
|
||||
|
||||
복사 대상 (Phase Z runtime 결과 → frontend type 매핑에 필요한 최소 set):
|
||||
root: final.html / preview.png / debug.json
|
||||
steps: step01_mdx_source.md (MDX 원문)
|
||||
step01_mdx_upload.json (run metadata)
|
||||
step02_normalized.json (sections)
|
||||
step05_v4_evidence.json (V4 후보 list)
|
||||
step06_composition_plan.json (composition units)
|
||||
step07_layout.json (layout preset + candidates)
|
||||
step08_zone_region_ratios.json (zone/region/display)
|
||||
step09_application_plan.json (적용 계획 — 핵심)
|
||||
step20_slide_status.json (최종 상태)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
RUNS_DIR = PROJECT_ROOT / "data" / "runs"
|
||||
|
||||
FRONTEND_DATA_DIR = Path(
|
||||
"D:/ad-hoc/kei/design_agent_front/design-agent/client/public/data/runs"
|
||||
)
|
||||
|
||||
ROOT_FILES = ["final.html", "preview.png", "debug.json"]
|
||||
|
||||
STEP_FILES = [
|
||||
"step01_mdx_source.md",
|
||||
"step01_mdx_upload.json",
|
||||
"step02_normalized.json",
|
||||
"step05_v4_evidence.json",
|
||||
"step06_composition_plan.json",
|
||||
"step07_layout.json",
|
||||
"step08_zone_region_ratios.json",
|
||||
"step09_application_plan.json",
|
||||
"step20_slide_status.json",
|
||||
]
|
||||
|
||||
|
||||
def list_runs() -> list[Path]:
|
||||
"""data/runs/*/phase_z2 가 있는 run dir 목록."""
|
||||
if not RUNS_DIR.exists():
|
||||
return []
|
||||
return sorted(
|
||||
p for p in RUNS_DIR.iterdir()
|
||||
if p.is_dir() and (p / "phase_z2").exists()
|
||||
)
|
||||
|
||||
|
||||
def sync_run(run_id: str, *, verbose: bool = True) -> int:
|
||||
"""run_id 의 phase_z2 산출물을 frontend 로 복사. 복사된 파일 수 반환."""
|
||||
src = RUNS_DIR / run_id / "phase_z2"
|
||||
if not src.exists():
|
||||
print(f"\n[error] source not found: {src}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
dst = FRONTEND_DATA_DIR / run_id
|
||||
dst.mkdir(parents=True, exist_ok=True)
|
||||
(dst / "steps").mkdir(exist_ok=True)
|
||||
|
||||
copied = 0
|
||||
missing: list[str] = []
|
||||
|
||||
for fname in ROOT_FILES:
|
||||
src_f = src / fname
|
||||
if src_f.exists():
|
||||
shutil.copy2(src_f, dst / fname)
|
||||
copied += 1
|
||||
else:
|
||||
missing.append(fname)
|
||||
|
||||
for fname in STEP_FILES:
|
||||
src_f = src / "steps" / fname
|
||||
if src_f.exists():
|
||||
shutil.copy2(src_f, dst / "steps" / fname)
|
||||
copied += 1
|
||||
else:
|
||||
missing.append(f"steps/{fname}")
|
||||
|
||||
if verbose:
|
||||
print(f"\n[ok] {run_id}")
|
||||
print(f" src : {src}")
|
||||
print(f" dst : {dst}")
|
||||
print(f" copied : {copied} files")
|
||||
if missing:
|
||||
print(f" missing: {len(missing)} files (산출물 없음 — 정상일 수 있음)")
|
||||
for m in missing:
|
||||
print(f" - {m}")
|
||||
return copied
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Phase Z run → frontend prototype 정적 export"
|
||||
)
|
||||
parser.add_argument(
|
||||
"run_id",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="복사할 run id. 미지정 시 가장 최근 mdx03_* run 사용.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="사용 가능한 run 목록만 출력 후 종료.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-mdx03",
|
||||
action="store_true",
|
||||
help="mdx03_* 로 시작하는 모든 run 복사.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list:
|
||||
runs = list_runs()
|
||||
print(f"\nAvailable runs ({len(runs)}):")
|
||||
for r in runs:
|
||||
print(f" - {r.name}")
|
||||
return
|
||||
|
||||
# client/public 자체는 존재해야 (frontend repo 가 정상이라는 sanity check)
|
||||
frontend_public = FRONTEND_DATA_DIR.parent.parent # = client/public
|
||||
if not frontend_public.exists():
|
||||
print(
|
||||
f"\n[error] frontend client/public 폴더 없음: {frontend_public}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" FRONTEND_DATA_DIR 경로 본 script 안에서 확인 필요.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
# data/runs 가 없으면 자동 생성 (정적 export 첫 실행 시)
|
||||
FRONTEND_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if args.all_mdx03:
|
||||
runs = [r for r in list_runs() if r.name.startswith("mdx03_")]
|
||||
print(f"\nSyncing {len(runs)} mdx03_* runs ...")
|
||||
total = 0
|
||||
for r in runs:
|
||||
total += sync_run(r.name, verbose=True)
|
||||
print(f"\n[done] total: {total} files across {len(runs)} runs")
|
||||
return
|
||||
|
||||
if args.run_id is None:
|
||||
# default: 가장 최근 mdx03_* run
|
||||
candidates = [r for r in list_runs() if r.name.startswith("mdx03_")]
|
||||
if not candidates:
|
||||
print(
|
||||
"\n[error] mdx03_* run 없음. 명시적으로 run_id 지정 필요.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
# mtime 기준 최신
|
||||
latest = max(candidates, key=lambda p: p.stat().st_mtime)
|
||||
args.run_id = latest.name
|
||||
print(f"\n[default] 가장 최근 mdx03_* run = {args.run_id}")
|
||||
|
||||
sync_run(args.run_id, verbose=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Extract content-preserving text atoms from MDX-like source.
|
||||
|
||||
The extractor is intentionally conservative: it ignores markdown/HTML/JSX
|
||||
syntax, style declarations, imports, and frontmatter, but preserves semantic
|
||||
text that a slide conversion pipeline must not add, drop, or rewrite.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
_FRONTMATTER_RE = re.compile(r"\A---\s*\n.*?\n---\s*\n", re.DOTALL)
|
||||
_TAG_RE = re.compile(r"<[^>]+>")
|
||||
_IMAGE_RE = re.compile(r"!\[([^\]]*)\]\([^)]+\)")
|
||||
_SUMMARY_TAG_RE = re.compile(r"<summary\b[^>]*>(.*?)</summary>", re.IGNORECASE)
|
||||
_NOTE_RE = re.compile(r"^:::\s*(?:note|tip|info|warning|danger)(?:\[(.*?)\])?\s*$", re.IGNORECASE)
|
||||
_TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)+\|?\s*$")
|
||||
_HEADING_RE = re.compile(r"^\s{0,3}#{1,6}\s+")
|
||||
_LIST_MARKER_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+")
|
||||
_EMPHASIS_RE = re.compile(r"[*_`~]+")
|
||||
_BR_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
|
||||
_STYLE_PROP_RE = re.compile(r"^\s*[A-Za-z][A-Za-z0-9_-]*\s*:\s*['\"#]?[A-Za-z0-9#().,% -]+['\"]?\s*,?\s*$")
|
||||
_JS_EVENT_RE = re.compile(r"^\s*(?:on[A-Z][A-Za-z]+|style|className)\s*=", re.ASCII)
|
||||
_IMPORT_EXPORT_RE = re.compile(r"^\s*(?:import|export)\s+")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TextAtom:
|
||||
source: str
|
||||
line_no: int
|
||||
kind: str
|
||||
text: str
|
||||
normalized: str
|
||||
digest: str
|
||||
|
||||
|
||||
def normalize_text_atom(text: str) -> str:
|
||||
"""Normalize syntax-only variation while preserving semantic content."""
|
||||
|
||||
text = html.unescape(text)
|
||||
text = text.replace("\u00a0", " ")
|
||||
text = text.replace("<br/>", " ").replace("<br />", " ").replace("<br>", " ")
|
||||
text = _TAG_RE.sub(" ", text)
|
||||
text = _EMPHASIS_RE.sub("", text)
|
||||
text = text.replace("<", "<").replace(">", ">").replace("&", "&")
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def atom_digest(normalized: str) -> str:
|
||||
return hashlib.sha1(normalized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def extract_text_atoms(path_or_text: str | Path, *, source_name: str | None = None) -> list[TextAtom]:
|
||||
if isinstance(path_or_text, Path):
|
||||
text = path_or_text.read_text(encoding="utf-8")
|
||||
source = source_name or path_or_text.name
|
||||
else:
|
||||
maybe_path = Path(path_or_text)
|
||||
if "\n" not in path_or_text and maybe_path.exists():
|
||||
text = maybe_path.read_text(encoding="utf-8")
|
||||
source = source_name or maybe_path.name
|
||||
else:
|
||||
text = path_or_text
|
||||
source = source_name or "<memory>"
|
||||
|
||||
text = _FRONTMATTER_RE.sub("", text)
|
||||
atoms: list[TextAtom] = []
|
||||
in_fenced_code = False
|
||||
|
||||
for line_no, raw_line in enumerate(text.splitlines(), start=1):
|
||||
line = raw_line.rstrip()
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if stripped.startswith("```"):
|
||||
in_fenced_code = not in_fenced_code
|
||||
continue
|
||||
if in_fenced_code:
|
||||
continue
|
||||
if _IMPORT_EXPORT_RE.match(stripped):
|
||||
continue
|
||||
if stripped in {"---", "<br/>", "<br />", "<br>", "</div>", "</details>"}:
|
||||
continue
|
||||
if stripped in {"{", "}", "});", "}}", "}}>", ">" }:
|
||||
continue
|
||||
if stripped.startswith(("e.currentTarget.", "<div style", "<p style", "<ul style", "<li style", "<h3 style")):
|
||||
continue
|
||||
if _JS_EVENT_RE.match(stripped) or _STYLE_PROP_RE.match(stripped):
|
||||
continue
|
||||
if stripped.startswith(("<div", "<p", "<ul", "<li", "<h")) and stripped.endswith(">") and not _TAG_RE.sub("", stripped).strip():
|
||||
continue
|
||||
if stripped.startswith("</"):
|
||||
continue
|
||||
|
||||
for alt in _IMAGE_RE.findall(stripped):
|
||||
_append_atom(atoms, source, line_no, "image_alt", alt)
|
||||
stripped = _IMAGE_RE.sub("", stripped).strip()
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
summary_match = _SUMMARY_TAG_RE.search(stripped)
|
||||
if summary_match:
|
||||
_append_atom(atoms, source, line_no, "details_summary", summary_match.group(1))
|
||||
stripped = _SUMMARY_TAG_RE.sub("", stripped).strip()
|
||||
if not stripped:
|
||||
continue
|
||||
|
||||
note_match = _NOTE_RE.match(stripped)
|
||||
if note_match:
|
||||
if note_match.group(1):
|
||||
_append_atom(atoms, source, line_no, "admonition_title", note_match.group(1))
|
||||
continue
|
||||
if stripped.startswith(":::"):
|
||||
continue
|
||||
|
||||
if "|" in stripped and stripped.count("|") >= 2:
|
||||
if _TABLE_SEPARATOR_RE.match(stripped):
|
||||
continue
|
||||
cells = [normalize_text_atom(c) for c in stripped.strip("|").split("|")]
|
||||
for cell in cells:
|
||||
if cell:
|
||||
_append_atom(atoms, source, line_no, "table_cell", cell)
|
||||
continue
|
||||
|
||||
kind = "paragraph"
|
||||
if _HEADING_RE.match(stripped):
|
||||
kind = "heading"
|
||||
stripped = _HEADING_RE.sub("", stripped)
|
||||
elif _LIST_MARKER_RE.match(stripped):
|
||||
kind = "list_item"
|
||||
stripped = _LIST_MARKER_RE.sub("", stripped)
|
||||
|
||||
stripped = re.sub(r"^\s*}+\s*>?\s*", "", stripped)
|
||||
stripped = _BR_RE.sub(" ", stripped)
|
||||
stripped = _TAG_RE.sub(" ", stripped)
|
||||
_append_atom(atoms, source, line_no, kind, stripped)
|
||||
|
||||
return atoms
|
||||
|
||||
|
||||
def compare_atom_sets(original_atoms: Iterable[TextAtom], standardized_atoms: Iterable[TextAtom]) -> dict[str, list[TextAtom]]:
|
||||
original_by_digest = {a.digest: a for a in original_atoms}
|
||||
standardized_by_digest = {a.digest: a for a in standardized_atoms}
|
||||
missing = [a for digest, a in original_by_digest.items() if digest not in standardized_by_digest]
|
||||
added = [a for digest, a in standardized_by_digest.items() if digest not in original_by_digest]
|
||||
return {"missing_from_standardized": missing, "added_in_standardized": added}
|
||||
|
||||
|
||||
def _append_atom(atoms: list[TextAtom], source: str, line_no: int, kind: str, text: str) -> None:
|
||||
normalized = normalize_text_atom(text)
|
||||
if not normalized:
|
||||
return
|
||||
if _looks_like_syntax_only(normalized):
|
||||
return
|
||||
atoms.append(
|
||||
TextAtom(
|
||||
source=source,
|
||||
line_no=line_no,
|
||||
kind=kind,
|
||||
text=text.strip(),
|
||||
normalized=normalized,
|
||||
digest=atom_digest(normalized),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _looks_like_syntax_only(text: str) -> bool:
|
||||
if text in {"/", "/>", ">", "{", "}", "});"}:
|
||||
return True
|
||||
if text.startswith(("style=", "onMouse", "cursor:", "fontWeight:", "color:", "margin", "padding")):
|
||||
return True
|
||||
if re.fullmatch(r"[{}(),;:'\"#.\-\s]+", text):
|
||||
return True
|
||||
return False
|
||||
@@ -29,8 +29,12 @@ SYSTEM_PROMPT = (
|
||||
f" 3. proposal_kind MUST be one of: {_ALLOWED_KINDS}.\n"
|
||||
f" 4. Do NOT propose any of: {_FORBIDDEN_KINDS}.\n"
|
||||
" 5. Do NOT change frame_id — V4 rank-1 frame is locked.\n"
|
||||
" 6. Keep declared frame slots (text/table/image/details) populated.\n"
|
||||
" 7. Respect Internal Region containment; place content units within "
|
||||
" 6. Preferred Task-12 output is proposal_kind='design_adaptation_plan': "
|
||||
"plan frame/layout/repeat changes only; do NOT output final text slots.\n"
|
||||
" 7. Do NOT create titles, labels, summaries, body text, or slot content. "
|
||||
"The code layer will place MDX verbatim text.\n"
|
||||
" 8. Keep declared frame slots (text/table/image/details) accounted for.\n"
|
||||
" 9. Respect Internal Region containment; place content units within "
|
||||
"the declared region only."
|
||||
)
|
||||
|
||||
@@ -73,6 +77,27 @@ def build_ai_fallback_prompt(
|
||||
"figma_partial_json": figma_partial_json,
|
||||
"internal_region": internal_region,
|
||||
"mdx_text_READ_ONLY": mdx_text,
|
||||
"task12_output_contract": {
|
||||
"preferred_proposal_kind": ProposalKind.DESIGN_ADAPTATION_PLAN.value,
|
||||
"allowed_operation_examples": [
|
||||
"increase_repeat_count",
|
||||
"split_group_across_repeat_blocks",
|
||||
"rebalance_zone_ratio",
|
||||
"compact_spacing",
|
||||
"use_repeatable_frame",
|
||||
],
|
||||
"forbidden_content_fields": [
|
||||
"text",
|
||||
"title",
|
||||
"label",
|
||||
"body",
|
||||
"slots",
|
||||
"mdx_text",
|
||||
"summary",
|
||||
"raw_html",
|
||||
"raw_css",
|
||||
],
|
||||
},
|
||||
}
|
||||
return {
|
||||
"system": SYSTEM_PROMPT,
|
||||
|
||||
@@ -4,6 +4,7 @@ Whitelisted proposal kinds (Stage 2 plan):
|
||||
- builder_options_patch : zone/frame builder option overrides
|
||||
- partial_overrides : Internal Region / Frame Slot content overrides
|
||||
- slot_mapping_proposal : restructuring proposal (content unit mapping)
|
||||
- design_adaptation_plan : structure-only plan; code applies verbatim MDX
|
||||
|
||||
Forbidden output forms (rejected by validator):
|
||||
- mdx_text (MDX read-only — `feedback_ai_isolation_contract`)
|
||||
@@ -23,6 +24,7 @@ class ProposalKind(str, Enum):
|
||||
BUILDER_OPTIONS_PATCH = "builder_options_patch"
|
||||
PARTIAL_OVERRIDES = "partial_overrides"
|
||||
SLOT_MAPPING_PROPOSAL = "slot_mapping_proposal"
|
||||
DESIGN_ADAPTATION_PLAN = "design_adaptation_plan"
|
||||
|
||||
|
||||
FORBIDDEN_KINDS: frozenset[str] = frozenset(
|
||||
|
||||
@@ -27,6 +27,43 @@ class AiFallbackValidationError(ValueError):
|
||||
|
||||
_SLOT_KINDS = (ProposalKind.PARTIAL_OVERRIDES, ProposalKind.SLOT_MAPPING_PROPOSAL)
|
||||
|
||||
_DESIGN_PLAN_FORBIDDEN_CONTENT_KEYS: frozenset[str] = frozenset(
|
||||
{
|
||||
"body",
|
||||
"label",
|
||||
"labels",
|
||||
"mdx_text",
|
||||
"new_text",
|
||||
"raw_css",
|
||||
"raw_html",
|
||||
"slot_payload",
|
||||
"slots",
|
||||
"summary",
|
||||
"text",
|
||||
"title",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _find_forbidden_design_plan_keys(value: Any, path: str = "payload") -> list[str]:
|
||||
"""Return content-bearing keys that would let AI generate text.
|
||||
|
||||
Task 12 contract: design_adaptation_plan may describe structure changes,
|
||||
but code remains the sole layer that maps MDX verbatim text into slots.
|
||||
"""
|
||||
hits: list[str] = []
|
||||
if isinstance(value, dict):
|
||||
for key, nested in value.items():
|
||||
key_str = str(key)
|
||||
nested_path = f"{path}.{key_str}"
|
||||
if key_str in _DESIGN_PLAN_FORBIDDEN_CONTENT_KEYS:
|
||||
hits.append(nested_path)
|
||||
hits.extend(_find_forbidden_design_plan_keys(nested, nested_path))
|
||||
elif isinstance(value, list):
|
||||
for idx, nested in enumerate(value):
|
||||
hits.extend(_find_forbidden_design_plan_keys(nested, f"{path}[{idx}]"))
|
||||
return hits
|
||||
|
||||
|
||||
def validate_proposal(
|
||||
proposal: AiFallbackProposal,
|
||||
@@ -73,6 +110,19 @@ def validate_proposal(
|
||||
"from payload.slots (text/table/image/details must remain populated)."
|
||||
)
|
||||
|
||||
if proposal.proposal_kind is ProposalKind.DESIGN_ADAPTATION_PLAN:
|
||||
operations = payload.get("operations")
|
||||
if not isinstance(operations, list) or not operations:
|
||||
raise AiFallbackValidationError(
|
||||
"design adaptation plan: payload.operations must be a non-empty list."
|
||||
)
|
||||
forbidden_paths = _find_forbidden_design_plan_keys(payload)
|
||||
if forbidden_paths:
|
||||
raise AiFallbackValidationError(
|
||||
"design adaptation plan: AI must not emit content-bearing fields "
|
||||
f"{forbidden_paths}; code maps MDX verbatim text."
|
||||
)
|
||||
|
||||
region_id = payload.get("region_id")
|
||||
if region_id is not None and internal_region is not None:
|
||||
declared_region_id = internal_region.get("id")
|
||||
|
||||
@@ -1039,10 +1039,22 @@ def select_composition_units(
|
||||
Args:
|
||||
candidates: full candidate pool from collect_candidates().
|
||||
allowed_statuses: phase_z_status set considered auto-renderable.
|
||||
all_section_ids: ordered section id list (only consulted when
|
||||
allow_provisional_fill=True; required for coverage check).
|
||||
all_section_ids: ordered section id list. Used for final source-order
|
||||
emission; also consulted for provisional coverage fill.
|
||||
allow_provisional_fill: opt-in for last-resort provisional fill.
|
||||
"""
|
||||
source_order_index = {sid: i for i, sid in enumerate(all_section_ids or [])}
|
||||
|
||||
def _source_order_key(c: CompositionUnit) -> tuple[int, int, str]:
|
||||
order_values = [
|
||||
source_order_index[sid]
|
||||
for sid in c.source_section_ids
|
||||
if sid in source_order_index
|
||||
]
|
||||
if not order_values:
|
||||
return (10**9, 10**9, "+".join(c.source_section_ids))
|
||||
return (min(order_values), max(order_values), "+".join(c.source_section_ids))
|
||||
|
||||
scored = [score_candidate(c) for c in candidates]
|
||||
viable = [
|
||||
c for c in scored
|
||||
@@ -1080,6 +1092,11 @@ def select_composition_units(
|
||||
selected.append(c)
|
||||
covered.update(c.source_section_ids)
|
||||
|
||||
# Task 9 (2026-05-28): frame score decides the covering frame, but final
|
||||
# unit emission must preserve MDX source order.
|
||||
if source_order_index:
|
||||
selected.sort(key=_source_order_key)
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
@@ -1163,7 +1180,7 @@ def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
|
||||
units = select_composition_units(
|
||||
candidates,
|
||||
allowed_statuses,
|
||||
all_section_ids=[s.section_id for s in sections] if allow_provisional_fill else None,
|
||||
all_section_ids=[s.section_id for s in sections],
|
||||
allow_provisional_fill=allow_provisional_fill,
|
||||
)
|
||||
preset = select_layout_preset(units)
|
||||
|
||||
@@ -172,11 +172,46 @@ def _split_label_for_bar(label: str) -> tuple[str, str]:
|
||||
return label.strip(), ""
|
||||
|
||||
|
||||
def normalize_markdown_inline_text(value: str | None) -> str:
|
||||
"""Strip authoring-only Markdown/HTML markers without rewriting words."""
|
||||
if value is None:
|
||||
return ""
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
if re.fullmatch(r"[-:|\s]+", text):
|
||||
return ""
|
||||
|
||||
text = re.sub(r"<br\s*/?>", " ", text, flags=re.IGNORECASE)
|
||||
text = text.replace(" ", " ")
|
||||
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"\1", text)
|
||||
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
text = re.sub(r"^\s*(?:[-*+]|\u2022)\s+", "", text)
|
||||
|
||||
if text.startswith("|") and text.endswith("|"):
|
||||
cells = [c.strip() for c in text.strip("|").split("|") if c.strip()]
|
||||
if all(re.fullmatch(r"[-: ]+", c) for c in cells):
|
||||
return ""
|
||||
text = " / ".join(cells)
|
||||
|
||||
previous = None
|
||||
while previous != text:
|
||||
previous = text
|
||||
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
|
||||
text = re.sub(r"__(.+?)__", r"\1", text)
|
||||
text = re.sub(r"(?<!\*)\*([^*\n]+?)\*(?!\*)", r"\1", text)
|
||||
text = re.sub(r"(?<!_)_([^_\n]+?)_(?!_)", r"\1", text)
|
||||
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def _extract_bold_or_plain(top_line: str) -> str:
|
||||
bold = re.search(r"\*\*(.+?)\*\*", top_line)
|
||||
if bold:
|
||||
return bold.group(1).strip()
|
||||
return top_line.strip().lstrip("*-").strip()
|
||||
return normalize_markdown_inline_text(bold.group(1))
|
||||
return normalize_markdown_inline_text(top_line)
|
||||
|
||||
|
||||
def _text_lines_with_indent(nested_lines: list[str], base_indent: int = 0) -> list[dict]:
|
||||
@@ -193,8 +228,9 @@ def _text_lines_with_indent(nested_lines: list[str], base_indent: int = 0) -> li
|
||||
rel = max(0, indent - base_indent)
|
||||
indent_level = max(0, rel // 2)
|
||||
text = re.sub(r"^[\*\-]\s+", "", s)
|
||||
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||
text_lines.append({"text": text, "indent": indent_level})
|
||||
text = normalize_markdown_inline_text(text)
|
||||
if text:
|
||||
text_lines.append({"text": text, "indent": indent_level})
|
||||
return text_lines
|
||||
|
||||
|
||||
@@ -213,12 +249,23 @@ def _extract_markdown_table(content: str) -> tuple[list[dict] | None, str]:
|
||||
return None, content
|
||||
rows = [r.strip() for r in m.group(1).strip().splitlines() if r.strip()]
|
||||
transforms = []
|
||||
header_cells = [c.strip() for c in rows[0].strip("|").split("|")]
|
||||
if len(header_cells) >= 3:
|
||||
header_from = normalize_markdown_inline_text(header_cells[0])
|
||||
header_mid = normalize_markdown_inline_text(header_cells[1])
|
||||
header_to = normalize_markdown_inline_text(header_cells[2])
|
||||
if header_from or header_mid or header_to:
|
||||
transforms.append({
|
||||
"from": " / ".join(x for x in [header_from, header_mid] if x),
|
||||
"to": header_to,
|
||||
})
|
||||
for r in rows[2:]:
|
||||
cells = [c.strip() for c in r.strip("|").split("|")]
|
||||
if len(cells) >= 3:
|
||||
f = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[0])
|
||||
t = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[2])
|
||||
transforms.append({"from": f, "to": t})
|
||||
f = normalize_markdown_inline_text(cells[0])
|
||||
t = normalize_markdown_inline_text(cells[2])
|
||||
if f or t:
|
||||
transforms.append({"from": f, "to": t})
|
||||
remaining = content[:m.start()] + content[m.end():]
|
||||
return (transforms or None), remaining
|
||||
|
||||
@@ -244,16 +291,18 @@ def _parse_nested_pillar_sections(nested_lines: list[str]) -> list[dict]:
|
||||
if cur_heading is not None:
|
||||
sections.append({"heading": cur_heading, "text_lines": cur_text_lines})
|
||||
bold = re.search(r"\*\*(.+?)\*\*", stripped)
|
||||
cur_heading = (bold.group(1).strip() if bold
|
||||
else stripped.lstrip("*-").strip())
|
||||
cur_heading = normalize_markdown_inline_text(
|
||||
bold.group(1) if bold else stripped
|
||||
)
|
||||
cur_text_lines = []
|
||||
section_base_indent = indent
|
||||
else:
|
||||
rel_indent = indent - section_base_indent
|
||||
indent_level = max(0, (rel_indent - 2) // 2)
|
||||
text = re.sub(r"^[\*\-]\s+", "", stripped)
|
||||
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||
cur_text_lines.append({"text": text, "indent": indent_level})
|
||||
text = normalize_markdown_inline_text(text)
|
||||
if text:
|
||||
cur_text_lines.append({"text": text, "indent": indent_level})
|
||||
|
||||
if cur_heading is not None:
|
||||
sections.append({"heading": cur_heading, "text_lines": cur_text_lines})
|
||||
@@ -291,6 +340,39 @@ def parse_quadrant_item(unit: tuple[str, list[str]]) -> dict:
|
||||
return {"label": label, "body": body}
|
||||
|
||||
|
||||
def _group_parsed_items_for_slots(parsed: list[dict], slot_count: int) -> list[dict]:
|
||||
"""Keep every source item while avoiding one visual card per tiny bullet.
|
||||
|
||||
Long sections can arrive as many sibling bullets even though the target
|
||||
frame has a small fixed data axis. In that case, preserve order and text by
|
||||
distributing the items across the existing slots instead of creating dozens
|
||||
of clipped cards.
|
||||
"""
|
||||
if slot_count <= 0 or len(parsed) <= slot_count * 2:
|
||||
return parsed
|
||||
|
||||
grouped: list[dict] = []
|
||||
for slot_index in range(slot_count):
|
||||
start = (len(parsed) * slot_index) // slot_count
|
||||
end = (len(parsed) * (slot_index + 1)) // slot_count
|
||||
bucket = parsed[start:end]
|
||||
if not bucket:
|
||||
grouped.append({"label": "", "body": []})
|
||||
continue
|
||||
|
||||
body: list[dict] = []
|
||||
first = bucket[0]
|
||||
body.extend(first.get("body") or [])
|
||||
for item in bucket[1:]:
|
||||
label = item.get("label") or ""
|
||||
if label:
|
||||
body.append({"text": label, "indent": 0})
|
||||
body.extend(item.get("body") or [])
|
||||
grouped.append({"label": first.get("label") or "", "body": body})
|
||||
|
||||
return grouped
|
||||
|
||||
|
||||
def parse_compare_row_2col_item(unit: tuple[str, list[str]]) -> dict:
|
||||
"""F18-style — bold = category label, nested 2 bullets = col_a / col_b values.
|
||||
|
||||
@@ -309,8 +391,9 @@ def parse_compare_row_2col_item(unit: tuple[str, list[str]]) -> dict:
|
||||
l_strip = l.strip()
|
||||
if re.match(r"^[\*\-]\s", l_strip):
|
||||
txt = re.sub(r"^[\*\-]\s+", "", l_strip)
|
||||
txt = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", txt)
|
||||
nested.append(txt)
|
||||
txt = normalize_markdown_inline_text(txt)
|
||||
if txt:
|
||||
nested.append(txt)
|
||||
col_a = nested[0] if len(nested) > 0 else ""
|
||||
col_b = nested[1] if len(nested) > 1 else ""
|
||||
return {"label": label, "col_a": col_a, "col_b": col_b}
|
||||
@@ -331,22 +414,57 @@ def _parse_column_sections(body: str, transform_first: bool) -> list[dict]:
|
||||
transform_first=True 면 첫 top-bullet 의 nested 안에 markdown table 이 있으면
|
||||
text_lines 대신 transforms 로 산출 (AS-IS/TO-BE).
|
||||
"""
|
||||
groups = _split_top_bullets(body)
|
||||
sections = []
|
||||
for i, (top_line, nested_lines) in enumerate(groups):
|
||||
title = _extract_bold_or_plain(top_line)
|
||||
if i == 0 and transform_first:
|
||||
nested_text = "\n".join(nested_lines)
|
||||
transforms, _ = _extract_markdown_table(nested_text)
|
||||
current_title: str | None = None
|
||||
current_lines: list[str] = []
|
||||
|
||||
def flush() -> None:
|
||||
nonlocal current_title, current_lines
|
||||
if current_title is None:
|
||||
return
|
||||
raw_body = "\n".join(current_lines)
|
||||
if transform_first and not sections:
|
||||
transforms, remaining = _extract_markdown_table(raw_body)
|
||||
if transforms:
|
||||
sections.append({"title": title, "transforms": transforms})
|
||||
continue
|
||||
non_empty = [l for l in nested_lines if l.strip()]
|
||||
base = min((len(l) - len(l.lstrip()) for l in non_empty), default=0)
|
||||
sections.append({
|
||||
"title": current_title,
|
||||
"transforms": transforms,
|
||||
"text_lines": _text_lines_with_indent(
|
||||
remaining.splitlines(),
|
||||
base_indent=0,
|
||||
),
|
||||
})
|
||||
current_title = None
|
||||
current_lines = []
|
||||
return
|
||||
sections.append({
|
||||
"title": title,
|
||||
"text_lines": _text_lines_with_indent(nested_lines, base_indent=base),
|
||||
"title": current_title,
|
||||
"text_lines": _text_lines_with_indent(current_lines, base_indent=0),
|
||||
})
|
||||
current_title = None
|
||||
current_lines = []
|
||||
|
||||
for line in body.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped == "---":
|
||||
continue
|
||||
if re.match(r"^[\*\-]\s+", stripped):
|
||||
text = re.sub(r"^[\*\-]\s+", "", stripped)
|
||||
text_norm = normalize_markdown_inline_text(text)
|
||||
bold = re.search(r"\*\*(.+?)\*\*", text)
|
||||
if bold:
|
||||
flush()
|
||||
current_title = normalize_markdown_inline_text(bold.group(1))
|
||||
current_lines = []
|
||||
elif current_title is not None and text_norm:
|
||||
current_lines.append(f"- {text_norm}")
|
||||
elif text_norm:
|
||||
current_title = text_norm
|
||||
current_lines = []
|
||||
elif current_title is not None:
|
||||
current_lines.append(line)
|
||||
|
||||
flush()
|
||||
return sections
|
||||
|
||||
|
||||
@@ -375,7 +493,7 @@ def _resolve_title(section, payload_spec: dict, contract: dict) -> dict:
|
||||
if src is None:
|
||||
return {}
|
||||
if src == "section.title":
|
||||
return {"title": section.title}
|
||||
return {"title": normalize_markdown_inline_text(section.title)}
|
||||
raise ValueError(
|
||||
f"Contract '{contract['template_id']}' has unsupported title source "
|
||||
f"'{src}'. v0 supports 'section.title' only."
|
||||
@@ -439,6 +557,10 @@ def _build_process_product_pair(section, units, contract) -> dict:
|
||||
|
||||
payload: dict = {}
|
||||
payload.update(_resolve_title(section, contract["payload"], contract))
|
||||
subheadings = [
|
||||
normalize_markdown_inline_text(m.group(1))
|
||||
for m in re.finditer(r"(?m)^###\s+(.+?)\s*$", getattr(section, "raw_content", "") or "")
|
||||
]
|
||||
|
||||
for i, col in enumerate(cols):
|
||||
sub_title, sub_body = units[i]
|
||||
@@ -455,7 +577,7 @@ def _build_process_product_pair(section, units, contract) -> dict:
|
||||
while len(sections_list) < pad_to:
|
||||
sections_list.append(dict(empty_template))
|
||||
sections_list = sections_list[:pad_to]
|
||||
payload[col["title_to"]] = sub_title
|
||||
payload[col["title_to"]] = subheadings[i] if i < len(subheadings) else sub_title
|
||||
payload[col["body_to"]] = {"sections": sections_list}
|
||||
|
||||
return payload
|
||||
@@ -487,6 +609,7 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
|
||||
|
||||
pad_to = options.get("pad_to", 4)
|
||||
truncate_at = options.get("truncate_at", pad_to)
|
||||
max_dynamic_slots = options.get("max_dynamic_slots")
|
||||
label_key = options.get("label_key_pattern", "quadrant_{n}_label")
|
||||
body_key = options.get("body_key_pattern", "quadrant_{n}_body")
|
||||
empty_label = options.get("empty_label", "")
|
||||
@@ -495,10 +618,29 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
|
||||
payload: dict = {}
|
||||
payload.update(_resolve_title(section, contract["payload"], contract))
|
||||
|
||||
visible_units = list(units[:truncate_at])
|
||||
visible_units = list(units)
|
||||
parsed = [parser(u) for u in visible_units]
|
||||
if max_dynamic_slots is None:
|
||||
# Keep the original fixed-card frame readable. Long content is grouped
|
||||
# into the declared slots rather than silently truncated or exploded
|
||||
# into dozens of clipped visual cards.
|
||||
max_dynamic_slots = pad_to
|
||||
dynamic_count = min(max(pad_to, len(parsed)), max_dynamic_slots)
|
||||
parsed = _group_parsed_items_for_slots(parsed, dynamic_count)
|
||||
subheadings = [
|
||||
normalize_markdown_inline_text(m.group(1))
|
||||
for m in re.finditer(r"(?m)^###\s+(.+?)\s*$", getattr(section, "raw_content", "") or "")
|
||||
]
|
||||
if subheadings and parsed:
|
||||
for i, heading in enumerate(subheadings):
|
||||
if not heading:
|
||||
continue
|
||||
target_index = min(len(parsed) - 1, (len(parsed) * i) // len(subheadings))
|
||||
body = parsed[target_index].setdefault("body", [])
|
||||
if not any(line.get("text") == heading for line in body if isinstance(line, dict)):
|
||||
body.insert(0, {"text": heading, "indent": 0})
|
||||
|
||||
for i in range(pad_to):
|
||||
for i in range(dynamic_count):
|
||||
n = i + 1
|
||||
if i < len(parsed):
|
||||
payload[label_key.format(n=n)] = parsed[i]["label"]
|
||||
@@ -508,8 +650,7 @@ def _build_quadrant_flat_slots(section, units, contract) -> dict:
|
||||
# list / dict default 는 항상 새 객체 — shared reference 방지
|
||||
payload[body_key.format(n=n)] = list(empty_body) if isinstance(empty_body, list) else empty_body
|
||||
|
||||
if len(units) > truncate_at:
|
||||
payload["_truncated_count"] = len(units) - truncate_at
|
||||
payload["_slot_count"] = dynamic_count
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ v0 minimal :
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
# B4 v0 input contract = B1 (ContentObject) + B2 (InternalRegion / ZoneRegionPlan).
|
||||
# 세 module 모두 dormant — runtime path 와 무관한 layer-agnostic 의존.
|
||||
@@ -40,6 +41,43 @@ from phase_z2_internal_region_planner import (
|
||||
)
|
||||
|
||||
|
||||
# ─── IMP-95 u1 — V4 evidence flag + trace key constants ─────────
|
||||
#
|
||||
# u1 scope-lock : env flag reader + trace key constant names only.
|
||||
# Selector wiring (u2), plan_placement signature (u3), Step 11 wiring
|
||||
# (u4), gatekeeper short-circuit (u5), partial_exists precheck (u6),
|
||||
# regression tests (u7~u10), and status-board markers (u11) are out of
|
||||
# scope for u1.
|
||||
|
||||
PHASE_Z_B4_V4_EVIDENCE_ENV = "PHASE_Z_B4_V4_EVIDENCE"
|
||||
|
||||
TRACE_KEY_FRAME_SELECTION_BASIS = "frame_selection_basis"
|
||||
TRACE_KEY_V4_EVIDENCE_CONSUMED = "v4_evidence_consumed"
|
||||
TRACE_KEY_B4_V0_FALLBACK_REASON = "b4_v0_fallback_reason"
|
||||
TRACE_KEY_V4_RANK_USED = "v4_rank_used"
|
||||
TRACE_KEY_V4_B4_FRAME_MATCH = "v4_b4_frame_match"
|
||||
TRACE_KEY_B4_PARTIAL_MISSING_SKIP = "b4_partial_missing_skip"
|
||||
|
||||
FRAME_SELECTION_BASIS_DECLARATION_ORDER = "declaration_order"
|
||||
FRAME_SELECTION_BASIS_V4_RANKED = "v4_ranked"
|
||||
|
||||
|
||||
def _b4_v4_evidence_enabled() -> bool:
|
||||
"""IMP-95 u1 — PHASE_Z_B4_V4_EVIDENCE env flag reader (default OFF).
|
||||
|
||||
Gates V4-aware frame selection in _select_frame (u2) and Step 11 wiring
|
||||
(u4). Default OFF preserves declaration-order behavior and final.html
|
||||
SHA parity. Independent of PHASE_Z_B4_MAPPER_SOURCE (IMP-89 89-a) and
|
||||
PHASE_Z_B4_GATEKEEPER. Truthy values: '1', 'true', 'yes' (case-insensitive,
|
||||
trimmed) — mirrors the contract at src/phase_z2_pipeline.py:_b4_mapper_source_enabled.
|
||||
"""
|
||||
return os.environ.get(PHASE_Z_B4_V4_EVIDENCE_ENV, "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
}
|
||||
|
||||
|
||||
# ─── Output schema (SPEC v1 §4.1) ────────────────────────────────
|
||||
|
||||
|
||||
@@ -71,6 +109,11 @@ class PlacementPlan:
|
||||
slot_assignments : Stage B 결과 (region.content_unit → Frame Slot)
|
||||
overflow_buffer : v0 = 빈 list (preview/details path 미활성)
|
||||
rejection : 매칭 안 된 / cardinality 초과 등 — 자동 렌더 X 신호
|
||||
selection_trace : IMP-95 u3 additive — frame selection telemetry.
|
||||
Always populated. Default = declaration_order
|
||||
(legacy behavior); V4-aware when env flag ON.
|
||||
Keys mirror u1 TRACE_KEY_* constants — Step 11
|
||||
wiring (u4) reads from this field, never inlines names.
|
||||
"""
|
||||
|
||||
section_id: str
|
||||
@@ -80,6 +123,7 @@ class PlacementPlan:
|
||||
slot_assignments: list[SlotAssignment] = field(default_factory=list)
|
||||
overflow_buffer: list[dict] = field(default_factory=list)
|
||||
rejection: list[dict] = field(default_factory=list)
|
||||
selection_trace: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ─── Frame selection ─────────────────────────────────────────────
|
||||
@@ -107,6 +151,138 @@ def _select_frame(
|
||||
return None
|
||||
|
||||
|
||||
# ─── IMP-95 u2 — V4 evidence-aware selector ──────────────────────
|
||||
#
|
||||
# Stage 2 u2 scope-lock : add a V4-aware selector that ranks eligible
|
||||
# contracts under the existing accepted_content_types ⊇ constraint,
|
||||
# then falls back to declaration order. plan_placement wiring (u3),
|
||||
# Step 11 trace exposure (u4), gatekeeper short-circuit (u5), and
|
||||
# partial_exists precheck (u6) are out of scope for u2.
|
||||
#
|
||||
# Behavior contract (IMP-95 issue body + Stage 1 guardrails) :
|
||||
# - V4 only re-orders among contracts that already satisfy
|
||||
# accepted_content_types ⊇ content_type_set.
|
||||
# - V4 ranks are tried in order; an ineligible / unmatched rank
|
||||
# falls through to the next rank (Stage 1 unresolved Q1 lock).
|
||||
# - When V4 evidence is empty / None / fails to resolve any rank,
|
||||
# fall back to declaration-order _select_frame() (preserves
|
||||
# final.html SHA parity under flag OFF when this helper is unused).
|
||||
# - Returns (Optional[frame_contract], selection_metadata dict)
|
||||
# using the u1 TRACE_KEY_* constants so u3 cannot drift names.
|
||||
|
||||
_FALLBACK_REASON_V4_EVIDENCE_EMPTY = "v4_evidence_empty"
|
||||
_FALLBACK_REASON_NO_V4_RANK_ELIGIBLE = "no_v4_rank_eligible"
|
||||
|
||||
|
||||
def _select_frame_v4_aware(
|
||||
content_objects: list[ContentObject],
|
||||
frame_contracts: list[dict[str, Any]],
|
||||
v4_candidates: Optional[list[Any]] = None,
|
||||
partial_exists: Optional[Callable[[str], bool]] = None,
|
||||
) -> tuple[Optional[dict[str, Any]], dict[str, Any]]:
|
||||
"""V4 evidence-aware variant of :func:`_select_frame`.
|
||||
|
||||
Algorithm :
|
||||
1. content_type_set = {obj.type for obj in content_objects} (re-uses
|
||||
the legacy ⊇ semantics — V4 only re-orders among eligible contracts).
|
||||
2. When ``v4_candidates`` is non-empty, iterate rank-ordered :
|
||||
a. Match candidate to a frame_contract by ``template_id`` first,
|
||||
``str(frame_id)`` second (duck-typed — composition.py:678-684).
|
||||
b. If the matched contract satisfies the ⊇ constraint, return it
|
||||
with basis = 'v4_ranked' + ``v4_rank_used`` = current index.
|
||||
c. Otherwise skip the candidate and continue to the next rank.
|
||||
3. Fall back to declaration-order selection via :func:`_select_frame`.
|
||||
The trace records why V4 evidence did not resolve
|
||||
('v4_evidence_empty' | 'no_v4_rank_eligible').
|
||||
|
||||
Returns :
|
||||
(frame_contract | None, selection_metadata dict)
|
||||
|
||||
Selection metadata keys (use u1 TRACE_KEY_* constants) :
|
||||
- frame_selection_basis : 'v4_ranked' | 'declaration_order'
|
||||
- v4_evidence_consumed : bool — True iff a V4 rank resolved.
|
||||
- v4_rank_used : int | None — 0-based rank index, else None.
|
||||
- v4_b4_frame_match : bool — True iff returned contract came from V4.
|
||||
- b4_v0_fallback_reason : str | None — set only when basis = declaration_order.
|
||||
- b4_partial_missing_skip : list[dict] — IMP-95 u6 ordered record of V4
|
||||
ranks skipped because their template_id had
|
||||
no partial HTML (contract-only template, 19
|
||||
in catalog as of 2026-05-25). Always present;
|
||||
empty when ``partial_exists`` is not supplied,
|
||||
no V4 evidence consulted, or no rank skipped.
|
||||
|
||||
IMP-95 u6 — ``partial_exists`` callable :
|
||||
When supplied and a V4 candidate resolves to a frame_contract by
|
||||
template_id/frame_id, the matched contract's ``template_id`` is passed
|
||||
to ``partial_exists`` BEFORE the accepted_content_types ⊇ check. If the
|
||||
callable returns False (no partial HTML on disk), the rank is recorded
|
||||
into ``b4_partial_missing_skip`` and the loop continues to the next
|
||||
rank. None (default) disables the precheck entirely — preserves the
|
||||
pre-u6 V4-aware selector behavior verbatim.
|
||||
"""
|
||||
content_type_set = {obj.type for obj in content_objects}
|
||||
partial_missing_skips: list[dict[str, Any]] = []
|
||||
|
||||
if v4_candidates:
|
||||
for rank_idx, cand in enumerate(v4_candidates):
|
||||
cand_tid = getattr(cand, "template_id", None)
|
||||
cand_fid = getattr(cand, "frame_id", None)
|
||||
matched: Optional[dict[str, Any]] = None
|
||||
# Two-pass match — template_id first across ALL contracts, then frame_id.
|
||||
# Single-pass per contract would let an earlier contract whose frame_id
|
||||
# matches the candidate win over a later contract whose template_id matches,
|
||||
# contradicting the documented precedence (composition.py:678-684).
|
||||
if cand_tid:
|
||||
for fc in frame_contracts:
|
||||
if fc.get("template_id") == cand_tid:
|
||||
matched = fc
|
||||
break
|
||||
if matched is None and cand_fid is not None:
|
||||
cand_fid_str = str(cand_fid)
|
||||
for fc in frame_contracts:
|
||||
fc_fid = fc.get("frame_id")
|
||||
if fc_fid is not None and str(fc_fid) == cand_fid_str:
|
||||
matched = fc
|
||||
break
|
||||
if matched is None:
|
||||
continue
|
||||
# IMP-95 u6 — partial_exists precheck. Skip contract-only templates
|
||||
# (no partial HTML on disk) before accepting the V4 rank. Trace the
|
||||
# skip in declaration order so downstream consumers can audit the
|
||||
# rank fall-through. Precheck is gated on partial_exists callable;
|
||||
# None (default) preserves the pre-u6 selector behavior.
|
||||
matched_tid = matched.get("template_id")
|
||||
if partial_exists is not None and matched_tid is not None:
|
||||
if not partial_exists(matched_tid):
|
||||
partial_missing_skips.append({
|
||||
"rank": rank_idx,
|
||||
"template_id": matched_tid,
|
||||
})
|
||||
continue
|
||||
accepted = set(matched.get("accepted_content_types") or [])
|
||||
if content_type_set <= accepted:
|
||||
return matched, {
|
||||
TRACE_KEY_FRAME_SELECTION_BASIS: FRAME_SELECTION_BASIS_V4_RANKED,
|
||||
TRACE_KEY_V4_EVIDENCE_CONSUMED: True,
|
||||
TRACE_KEY_V4_RANK_USED: rank_idx,
|
||||
TRACE_KEY_V4_B4_FRAME_MATCH: True,
|
||||
TRACE_KEY_B4_V0_FALLBACK_REASON: None,
|
||||
TRACE_KEY_B4_PARTIAL_MISSING_SKIP: partial_missing_skips,
|
||||
}
|
||||
fallback_reason = _FALLBACK_REASON_NO_V4_RANK_ELIGIBLE
|
||||
else:
|
||||
fallback_reason = _FALLBACK_REASON_V4_EVIDENCE_EMPTY
|
||||
|
||||
return _select_frame(content_objects, frame_contracts), {
|
||||
TRACE_KEY_FRAME_SELECTION_BASIS: FRAME_SELECTION_BASIS_DECLARATION_ORDER,
|
||||
TRACE_KEY_V4_EVIDENCE_CONSUMED: False,
|
||||
TRACE_KEY_V4_RANK_USED: None,
|
||||
TRACE_KEY_V4_B4_FRAME_MATCH: False,
|
||||
TRACE_KEY_B4_V0_FALLBACK_REASON: fallback_reason,
|
||||
TRACE_KEY_B4_PARTIAL_MISSING_SKIP: partial_missing_skips,
|
||||
}
|
||||
|
||||
|
||||
# ─── Sub_zone assignment (Stage B) ───────────────────────────────
|
||||
|
||||
|
||||
@@ -161,16 +337,43 @@ def _assign_region_to_sub_zone(
|
||||
# ─── Public entry ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _declaration_order_selection_trace() -> dict[str, Any]:
|
||||
"""IMP-95 u3 — selection_trace default (legacy declaration-order path).
|
||||
|
||||
Returned when ``PHASE_Z_B4_V4_EVIDENCE`` is OFF or when no V4 evidence
|
||||
is offered. Keys are stable u1 ``TRACE_KEY_*`` constants so u4 (Step 11
|
||||
wiring) cannot drift names. ``b4_v0_fallback_reason`` is ``None`` here
|
||||
because the V4 path is not attempted at all under this branch — the
|
||||
fallback-reason enum is reserved for the V4-aware selector's own
|
||||
fall-through bookkeeping (u2 contract).
|
||||
"""
|
||||
return {
|
||||
TRACE_KEY_FRAME_SELECTION_BASIS: FRAME_SELECTION_BASIS_DECLARATION_ORDER,
|
||||
TRACE_KEY_V4_EVIDENCE_CONSUMED: False,
|
||||
TRACE_KEY_V4_RANK_USED: None,
|
||||
TRACE_KEY_V4_B4_FRAME_MATCH: False,
|
||||
TRACE_KEY_B4_V0_FALLBACK_REASON: None,
|
||||
TRACE_KEY_B4_PARTIAL_MISSING_SKIP: [],
|
||||
}
|
||||
|
||||
|
||||
def plan_placement(
|
||||
content_objects: list[ContentObject],
|
||||
frame_contracts: list[dict[str, Any]],
|
||||
section_id: str = "",
|
||||
v4_candidates: Optional[list[Any]] = None,
|
||||
partial_exists: Optional[Callable[[str], bool]] = None,
|
||||
) -> PlacementPlan:
|
||||
"""ContentObject[] + frame_contracts → PlacementPlan (Stage A + Stage B 통합).
|
||||
|
||||
v0 algorithm :
|
||||
1. Stage A = B2 plan_internal_regions() 호출 → internal_regions 획득
|
||||
2. frame 선택 : accepted_content_types cover + 입력 순서 first
|
||||
2. frame 선택 : accepted_content_types cover + 입력 순서 first.
|
||||
IMP-95 u3 — when ``PHASE_Z_B4_V4_EVIDENCE`` env flag is ON,
|
||||
delegates to :func:`_select_frame_v4_aware` with the optional
|
||||
``v4_candidates`` evidence. Flag OFF (default) preserves the
|
||||
declaration-order ``_select_frame`` call site (final.html SHA
|
||||
parity guarantee, Stage 2 A1 + A10).
|
||||
- cover 실패 시 → rejection + early return
|
||||
3. selected_frame 의 sub_zones 읽음 (B3 catalog)
|
||||
4. Stage B (region 1:1 sub_zone 매핑) :
|
||||
@@ -186,11 +389,31 @@ def plan_placement(
|
||||
frame_contracts : list[dict] — frame_contracts.yaml 의 contract dict list
|
||||
(YAML declaration order = list 순서로 입력 권고)
|
||||
section_id : region_id / 결과 식별자 prefix
|
||||
v4_candidates : optional V4 rank-ordered evidence (duck-typed —
|
||||
``template_id`` + ``frame_id`` per
|
||||
``src/phase_z2_composition.py:678-684``). Consumed
|
||||
ONLY when ``PHASE_Z_B4_V4_EVIDENCE`` flag is ON
|
||||
(default OFF → ignored, declaration order preserved).
|
||||
Step 11 wiring (u4) forwards ``unit.v4_candidates``;
|
||||
existing call sites that omit the kwarg keep
|
||||
identical legacy behavior.
|
||||
partial_exists : optional ``Callable[[str], bool]`` — IMP-95 u6
|
||||
contract-only / no-partial precheck. When supplied
|
||||
AND ``PHASE_Z_B4_V4_EVIDENCE`` is ON, V4 ranks whose
|
||||
resolved ``template_id`` returns False are skipped
|
||||
(recorded into ``b4_partial_missing_skip``) and the
|
||||
loop falls through to the next rank. None (default)
|
||||
disables the precheck — pre-u6 selector behavior.
|
||||
Declaration-order legacy path (flag OFF) is NEVER
|
||||
pre-checked, preserving final.html SHA parity.
|
||||
|
||||
Returns :
|
||||
PlacementPlan
|
||||
PlacementPlan — ``selection_trace`` (additive) records which path
|
||||
(declaration_order vs v4_ranked) selected the frame and why a V4
|
||||
candidate was / was not consumed.
|
||||
"""
|
||||
plan = PlacementPlan(section_id=section_id)
|
||||
plan.selection_trace = _declaration_order_selection_trace()
|
||||
|
||||
if not content_objects:
|
||||
return plan
|
||||
@@ -203,8 +426,22 @@ def plan_placement(
|
||||
)
|
||||
plan.internal_regions = list(zone_plan.internal_regions)
|
||||
|
||||
# 2. frame 선택
|
||||
selected_frame = _select_frame(content_objects, frame_contracts)
|
||||
# 2. frame 선택 — IMP-95 u3 flag-gated V4-aware path.
|
||||
# Flag OFF preserves declaration-order _select_frame call (legacy parity).
|
||||
# Flag ON routes to _select_frame_v4_aware (u2) with optional V4 evidence;
|
||||
# when v4_candidates is empty/None the selector itself emits the
|
||||
# 'v4_evidence_empty' fallback reason and returns the same declaration-order
|
||||
# frame, so flag-ON + no-evidence remains behaviorally equivalent for
|
||||
# selected_frame_id / selected_template_id.
|
||||
if _b4_v4_evidence_enabled():
|
||||
selected_frame, plan.selection_trace = _select_frame_v4_aware(
|
||||
content_objects,
|
||||
frame_contracts,
|
||||
v4_candidates,
|
||||
partial_exists=partial_exists,
|
||||
)
|
||||
else:
|
||||
selected_frame = _select_frame(content_objects, frame_contracts)
|
||||
if selected_frame is None:
|
||||
plan.rejection.append({
|
||||
"reason": "no_frame_covers_content_types",
|
||||
|
||||
@@ -47,36 +47,29 @@ def plan_zone_ratio_retry(
|
||||
fit_classification: dict,
|
||||
router_decision: dict,
|
||||
safety_margin_px: int = DEFAULT_SAFETY_MARGIN_PX,
|
||||
override_zone_geometries: Optional[dict[str, dict]] = None,
|
||||
) -> Optional[dict]:
|
||||
"""zone_ratio_retry 의 redistribution plan 을 산출.
|
||||
"""Build a deterministic zone-height redistribution plan.
|
||||
|
||||
*plan 만*. 실제 height 적용 / rerender X (caller 가 처리).
|
||||
|
||||
Returns:
|
||||
None : retry 시도 자체가 불필요 (router 가 zone_ratio_retry 제안 X)
|
||||
dict : retry attempt 정보 (feasible 여부 + 상세)
|
||||
|
||||
feasible=True 이면 caller 가 zones_after 로 layout_css 재구성 + rerender 시도.
|
||||
feasible=False 이면 caller 는 retry 포기 (original final.html 유지).
|
||||
T28.5b: zones listed in ``override_zone_geometries`` are manual/frozen.
|
||||
A frozen zone can be neither the overflow target nor a donor.
|
||||
"""
|
||||
if not router_decision.get("router_active"):
|
||||
return None
|
||||
|
||||
# zone_ratio_retry 가 router 제안에 포함된 첫 classification 을 target 으로
|
||||
target_cls = None
|
||||
for cls in fit_classification.get("classifications", []) or []:
|
||||
if cls.get("proposed_action") == "zone_ratio_retry":
|
||||
target_cls = cls
|
||||
break
|
||||
if target_cls is None:
|
||||
return None # 다른 action (popup / reselect) — 본 retry 대상 아님
|
||||
return None
|
||||
|
||||
target_zone_position = target_cls.get("zone_position")
|
||||
target_excess_y = float(target_cls.get("inputs", {}).get("excess_y", 0))
|
||||
# round up to integer (subpixel 끼면 부족할 수 있음)
|
||||
target_added_px = int(math.ceil(target_excess_y)) + int(safety_margin_px)
|
||||
manual_positions = set((override_zone_geometries or {}).keys())
|
||||
|
||||
# zones_before — debug_zones 의 height_px 를 모음
|
||||
zones_before: dict[str, int] = {}
|
||||
zone_min_by_pos: dict[str, int] = {}
|
||||
for dz in debug_zones:
|
||||
@@ -90,40 +83,52 @@ def plan_zone_ratio_retry(
|
||||
zones_before[pos] = int(h)
|
||||
zone_min_by_pos[pos] = int(m)
|
||||
|
||||
# overflow zone 별 visual fail 정보
|
||||
overflow_zone_status: dict[str, dict] = {}
|
||||
for z in overflow.get("zones", []) or []:
|
||||
overflow_zone_status[z.get("position")] = z
|
||||
overflow_zone_status = {
|
||||
z.get("position"): z for z in overflow.get("zones", []) or []
|
||||
}
|
||||
base_plan = {
|
||||
"target_zone_position": target_zone_position,
|
||||
"target_excess_y": target_excess_y,
|
||||
"target_added_px": target_added_px,
|
||||
"safety_margin_px_used": int(safety_margin_px),
|
||||
"zones_before": dict(zones_before),
|
||||
"manual_zone_positions": sorted(manual_positions),
|
||||
"manual_geometry_policy": "per_zone_freeze",
|
||||
}
|
||||
|
||||
if target_zone_position in manual_positions:
|
||||
return {
|
||||
**base_plan,
|
||||
"feasible": False,
|
||||
"manual_target_blocked": True,
|
||||
"donor_zone_position": None,
|
||||
"donor_reduced_px": 0,
|
||||
"donor_candidates_considered": [],
|
||||
"zones_after": dict(zones_before),
|
||||
"failure_reason": (
|
||||
f"manual target zone '{target_zone_position}' is frozen by "
|
||||
"override_zone_geometries; emit warning/recommended_geometry only"
|
||||
),
|
||||
}
|
||||
|
||||
# donor 후보 식별
|
||||
donor_candidates: list[dict] = []
|
||||
for dz in debug_zones:
|
||||
pos = dz.get("position")
|
||||
if pos is None or pos == target_zone_position:
|
||||
continue
|
||||
# rule 4-(a) sibling 확인은 layout 내 sibling = 같은 zones list 안에 있으면 OK
|
||||
# (본 함수는 1 layout 내 zones 만 받음)
|
||||
if pos in manual_positions:
|
||||
continue
|
||||
|
||||
# rule 4-(b) visual_check 통과 — 이 zone 에 자체 overflow / clipped_inner 없음
|
||||
zinfo = overflow_zone_status.get(pos, {})
|
||||
zone_self_overflow = bool(zinfo.get("overflowed"))
|
||||
zone_inner_clipped = bool(zinfo.get("clipped_inner"))
|
||||
if zone_self_overflow or zone_inner_clipped:
|
||||
if bool(zinfo.get("overflowed")) or bool(zinfo.get("clipped_inner")):
|
||||
continue
|
||||
|
||||
# rule 4-(c) capacity_fit 가 ok
|
||||
cap_status = (
|
||||
(dz.get("composition_rationale") or {}).get("capacity_fit", {}).get("fit_status")
|
||||
)
|
||||
# 'ok' 아니거나 missing/unknown 이면 보수적으로 제외 (no_contract 는 허용 — capacity_fit 자체 부재)
|
||||
if cap_status not in {"ok", "no_contract", None}:
|
||||
continue
|
||||
|
||||
# rule 4-(d) 현재 height > min_height
|
||||
# IMP-34 u1: donor capacity bounded by measured empty space
|
||||
# (clientHeight - scrollHeight from Step 14) when both fields are present,
|
||||
# falling back to static contract slack when absent. Prevents the donor
|
||||
# from being over-allocated when it is already full but not overflowing.
|
||||
height = zones_before.get(pos)
|
||||
min_h = zone_min_by_pos.get(pos)
|
||||
if height is None or min_h is None:
|
||||
@@ -157,18 +162,8 @@ def plan_zone_ratio_retry(
|
||||
"slack_bound_source": slack_bound_source,
|
||||
})
|
||||
|
||||
# rule 4-(f) 여러 후보면 slack 가장 큰 것부터
|
||||
donor_candidates.sort(key=lambda d: d["slack"], reverse=True)
|
||||
|
||||
# base plan dict (failure / success 공용)
|
||||
base_plan = {
|
||||
"target_zone_position": target_zone_position,
|
||||
"target_excess_y": target_excess_y,
|
||||
"target_added_px": target_added_px,
|
||||
"safety_margin_px_used": int(safety_margin_px),
|
||||
"donor_candidates_considered": donor_candidates,
|
||||
"zones_before": dict(zones_before),
|
||||
}
|
||||
base_plan["donor_candidates_considered"] = donor_candidates
|
||||
|
||||
if not donor_candidates:
|
||||
return {
|
||||
@@ -178,12 +173,11 @@ def plan_zone_ratio_retry(
|
||||
"donor_reduced_px": 0,
|
||||
"zones_after": dict(zones_before),
|
||||
"failure_reason": (
|
||||
f"no donor candidates eligible (sibling visual_check OK + "
|
||||
f"capacity_fit ok/no_contract + slack > 0)"
|
||||
"no donor candidates eligible (sibling visual_check OK + "
|
||||
"capacity_fit ok/no_contract + slack > 0 + not manual/frozen)"
|
||||
),
|
||||
}
|
||||
|
||||
# IMP-12 u1 : multi-donor greedy aggregation (slack-desc 순서대로 합산)
|
||||
aggregate_slack_available = sum(d["slack"] for d in donor_candidates)
|
||||
if aggregate_slack_available < target_added_px:
|
||||
return {
|
||||
@@ -205,7 +199,6 @@ def plan_zone_ratio_retry(
|
||||
),
|
||||
}
|
||||
|
||||
# feasible — greedy aggregation: 각 donor 에서 필요한 만큼만 차감
|
||||
zones_after = dict(zones_before)
|
||||
zones_after[target_zone_position] = zones_before[target_zone_position] + target_added_px
|
||||
donors_used: list[dict] = []
|
||||
@@ -270,6 +263,7 @@ def plan_cross_zone_redistribute(
|
||||
fit_analysis,
|
||||
containers: dict,
|
||||
min_margin_px: float | None = None,
|
||||
manual_zone_positions: set[str] | list[str] | tuple[str, ...] | None = None,
|
||||
) -> dict:
|
||||
"""Cross-zone (intra-zone role-to-role) redistribute plan.
|
||||
|
||||
@@ -279,19 +273,38 @@ def plan_cross_zone_redistribute(
|
||||
from copy import deepcopy
|
||||
from src.fit_verifier import redistribute as _fv_redistribute
|
||||
|
||||
manual_positions = set(manual_zone_positions or [])
|
||||
role_heights_before = {
|
||||
role: float(rf.allocated_px) for role, rf in (fit_analysis.roles or {}).items()
|
||||
role: float(rf.allocated_px)
|
||||
for role, rf in (fit_analysis.roles or {}).items()
|
||||
if role not in manual_positions
|
||||
}
|
||||
base_plan = {
|
||||
"action": "cross_zone_redistribute",
|
||||
"role_heights_before": role_heights_before,
|
||||
"manual_zone_positions": sorted(manual_positions),
|
||||
"manual_geometry_policy": "per_zone_freeze",
|
||||
}
|
||||
if not role_heights_before:
|
||||
return {**base_plan, "feasible": False, "role_heights_after": {},
|
||||
"can_redistribute": False,
|
||||
"failure_reason": "no roles in fit_analysis — cannot redistribute."}
|
||||
|
||||
result = _fv_redistribute(deepcopy(fit_analysis), containers, min_margin_px=min_margin_px)
|
||||
fit_for_redistribute = deepcopy(fit_analysis)
|
||||
fit_for_redistribute.roles = {
|
||||
role: rf
|
||||
for role, rf in (fit_for_redistribute.roles or {}).items()
|
||||
if role not in manual_positions
|
||||
}
|
||||
automatic_containers = {
|
||||
role: value for role, value in (containers or {}).items()
|
||||
if role not in manual_positions
|
||||
}
|
||||
result = _fv_redistribute(
|
||||
fit_for_redistribute,
|
||||
automatic_containers,
|
||||
min_margin_px=min_margin_px,
|
||||
)
|
||||
redistribution = dict(result.redistribution or {})
|
||||
can_redistribute = bool(result.can_redistribute)
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Block Catalog v1
|
||||
# 파이프라인이 콘텐츠에 맞는 블록을 선택할 때 사용하는 메타데이터.
|
||||
# 각 블록의 구조, 슬롯, 호환 recipe를 정의.
|
||||
|
||||
blocks:
|
||||
|
||||
- id: prerequisites-3col
|
||||
source_frame: "1171281190"
|
||||
structure_type: 3col-parallel
|
||||
description: "3열 병렬 비교. 각 열에 gradient 바 + 2 섹션(제목+설명). 기술/사람/환경 같은 병렬 조건 나열에 적합."
|
||||
keywords: [필수, 요건, 조건, 기술, 사람, 자연, 역량, 디지털, 투자, 병렬, 3가지]
|
||||
slots:
|
||||
- zone_title # 블록 대제목 (e.g. "필수조건")
|
||||
- sub_title # 열별 섹션 소제목 (×6: 3열 × 2섹션)
|
||||
- body # 열별 섹션 설명 텍스트 (×6)
|
||||
recipe_compat: [direct_fit, parallel_cluster]
|
||||
not_for: [long_table, image_heavy, comparison_2col, sequential_flow]
|
||||
|
||||
- id: compare-vs-rows
|
||||
source_frame: "1171281195"
|
||||
structure_type: compare-2col-badge
|
||||
description: "좌/우 비교 행 N개 + 중앙 뱃지. 서브헤더 + 결론 포함. BIM vs DX 같은 이분법 비교에 적합."
|
||||
keywords: [비교, 차이, BIM, DX, 대비, 프로세스, 성과물, 개념, 활용]
|
||||
slots:
|
||||
- zone_title # 블록 대제목
|
||||
- sub_title # 서브헤더 (e.g. "BIM vs DX")
|
||||
- bullets # 좌/우 비교 항목 (×N행)
|
||||
recipe_compat: [direct_fit, two_col_text_visual, comparison]
|
||||
not_for: [image_heavy, single_column, 3col_layout]
|
||||
|
||||
- id: compare-table-3col
|
||||
source_frame: "1171281204"
|
||||
structure_type: table-3col
|
||||
description: "CSS grid 3열 비교 표. 행 라벨 + 3열 셀. R13 flex pair 불릿. S/W 유형 비교 같은 다축 비교에 적합."
|
||||
keywords: [비교, 구분, 상용, 전용, 개발, 개념, 성과품, 표, 3열]
|
||||
slots:
|
||||
- zone_title # 블록 대제목
|
||||
- preview # 표 헤더 + 행 데이터 (행 라벨, 3열 셀)
|
||||
recipe_compat: [direct_fit, comparison]
|
||||
not_for: [image_heavy, single_column, narrative_flow]
|
||||
|
||||
- id: compare-table-2col
|
||||
source_frame: "1171281203"
|
||||
structure_type: table-2col
|
||||
description: "CSS grid 2열 비교 표. 행 라벨 + 2열 셀. R13 flex pair 불릿. Package vs Solution 같은 이분법 표에 적합."
|
||||
keywords: [비교, 구분, 정의, 특징, 장점, 단점, 표, 2열, 패키지, 솔루션]
|
||||
slots:
|
||||
- zone_title # 블록 대제목
|
||||
- preview # 표 헤더 + 행 데이터
|
||||
recipe_compat: [direct_fit, comparison]
|
||||
not_for: [image_heavy, single_column, narrative_flow]
|
||||
|
||||
- id: stacked-arrow-list
|
||||
source_frame: "1171281180"
|
||||
structure_type: list-stacked
|
||||
description: "계단식 pill 행 리스트. 화살표 마커 + 텍스트. 좌측 아크 장식 선택적. 항목 나열/흐름에 적합."
|
||||
keywords: [시공, 정보물, 모델, 시뮬레이션, 도면, 성과물, 목록, 항목, 나열]
|
||||
slots:
|
||||
- zone_title # 블록 대제목
|
||||
- bullets # pill 행 텍스트 (×N)
|
||||
recipe_compat: [direct_fit, list_flow]
|
||||
not_for: [comparison, image_heavy, long_table]
|
||||
|
||||
- id: issues-paired-rows
|
||||
source_frame: "1171281194"
|
||||
structure_type: 2col-paired
|
||||
description: "좌/우 이슈 행 N개. pill 라벨 + 본문, 가운데 구분선. 현황/문제점 같은 대칭 이슈 나열에 적합."
|
||||
keywords: [현황, 문제점, 이슈, 개념, 접근, 방향, 방식, 오류, 대칭]
|
||||
slots:
|
||||
- zone_title # 블록 대제목
|
||||
- sub_title # pill 라벨 (×N행 × 좌우)
|
||||
- body # 좌/우 본문 (×N행)
|
||||
recipe_compat: [direct_fit, two_col_text_visual]
|
||||
not_for: [long_table, image_heavy, 3col_layout]
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 534 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,24 @@
|
||||
# Component expansion registry for MDX-to-frame structure normalization.
|
||||
#
|
||||
# This file is introduced by #98 T21.6 as a contract artifact. T23 will wire
|
||||
# the parser/normalizer to read it. Entries must describe how an opaque
|
||||
# component becomes typed source nodes while preserving source text verbatim.
|
||||
|
||||
version: 1
|
||||
|
||||
components:
|
||||
DxEffect:
|
||||
status: planned
|
||||
source_path: samples/src/components/dx.astro
|
||||
expands_to:
|
||||
- table_like_content
|
||||
- persona_column
|
||||
persona_role_labels:
|
||||
- 발주자
|
||||
- 시공자
|
||||
- 설계자
|
||||
parent_policy: keep_under_containing_numbered_section
|
||||
text_policy: verbatim_from_component_source
|
||||
notes:
|
||||
- Initial registry entry observed in MDX 02.
|
||||
- T23 must replace opaque component handling with this registry-based expansion.
|
||||
@@ -31,6 +31,15 @@ three_parallel_requirements:
|
||||
cardinality:
|
||||
strict: 3
|
||||
overflow_policy: abort_or_review
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: pillars
|
||||
base_items: 3
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: card
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
# 순서 기반 visual role (color_class 대체 — label 키워드 의존 제거)
|
||||
role_order:
|
||||
@@ -143,6 +152,18 @@ bim_issues_quadrant_four:
|
||||
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
|
||||
|
||||
source_shape: top_bullets
|
||||
cardinality:
|
||||
strict: 4
|
||||
overflow_policy: abort_or_review
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: quadrants
|
||||
base_items: 4
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: card
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
# F16 정책 = pad_to=4 + truncate>4 (legacy 와 동일). cardinality strict 화는 본 transition 범위 외.
|
||||
# 향후 normal path 안정 후 strict 적용 + 위반 시 fallback path (FitError) 검토.
|
||||
|
||||
@@ -213,6 +234,15 @@ three_persona_benefits:
|
||||
cardinality:
|
||||
strict: 3 # 3 persona = strict.
|
||||
overflow_policy: abort_or_review
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: personas
|
||||
base_items: 3
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: card
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
# 순서 기반 color theme (Figma 원본 색).
|
||||
# client = 발주자 (#285b4a 그린 계열), constructor = 시공자 (#445a2f 올리브),
|
||||
@@ -285,6 +315,16 @@ construction_goals_three_circle_intersection:
|
||||
|
||||
# 순서 기반 visual role (3 메인 원 각자 다른 gradient — index.html L141-189).
|
||||
# circle_1 = 안전(orange/red), circle_2 = 생산성(brown/grey), circle_3 = 소통(green/dark).
|
||||
dynamic_cardinality:
|
||||
repeatable: false
|
||||
repeat_axis: circles
|
||||
base_items: 3
|
||||
min_items: 3
|
||||
max_items: 3
|
||||
expand_policy: fixed_diagram
|
||||
overflow_policy: fallback_to_repeatable_frame
|
||||
preserve_source_order: true
|
||||
|
||||
role_order:
|
||||
- safety
|
||||
- productivity
|
||||
@@ -360,6 +400,16 @@ construction_bim_three_usage:
|
||||
# 순서 기반 visual role — 3 카테고리 (model / object / position) 각자
|
||||
# Figma 의 동일 brown label box + white content card 패턴. role 은
|
||||
# MDX label 내용 따라 결정되므로 generic ordering name 만.
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: categories
|
||||
base_items: 3
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: row
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
role_order:
|
||||
- category_1
|
||||
- category_2
|
||||
@@ -432,6 +482,16 @@ bim_dx_comparison_table:
|
||||
strict: 2 # 2 columns (col_a / col_b) — NOT row count
|
||||
overflow_policy: abort_or_review
|
||||
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: rows
|
||||
base_items: 4
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: table_row
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
role_order:
|
||||
- col_a
|
||||
- col_b
|
||||
@@ -842,6 +902,15 @@ pre_construction_model_info_stacked:
|
||||
min: 4 # analysis.md min 4 / Figma 5-color cycle design floor.
|
||||
max: 5 # partial `_slot_count or 5` hardcap (5-color cycle, line 175).
|
||||
overflow_policy: abort_or_review
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: pills
|
||||
base_items: 5
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: list_row
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
visual_hints:
|
||||
min_height_px: 350 # title 30 + 5 pill × ~55 + grid gap 40 + padding 30 + buffer 5.
|
||||
@@ -1624,6 +1693,16 @@ sw_dependency_four_problems:
|
||||
strict: 4 # 4 problem (problem_1..problem_4) — data axis. analysis.md ideal 4 / min 4 / max 4 (strict:4 = declared data slot count, peer parity with u10/u12/u13/u17/u19 ideal=min=max convention); title = fixed singleton header (non-data axis). problem_N_label + problem_N_body 평탄 = builder/parser 내부 (composite problem_N = Frame Slot 단위, per F16 precedent).
|
||||
overflow_policy: abort_or_review
|
||||
|
||||
dynamic_cardinality:
|
||||
repeatable: true
|
||||
repeat_axis: problems
|
||||
base_items: 4
|
||||
min_items: 1
|
||||
max_items: null
|
||||
expand_policy: card
|
||||
overflow_policy: expand_then_fit
|
||||
preserve_source_order: true
|
||||
|
||||
role_order: [problem_1, problem_2, problem_3, problem_4]
|
||||
visual_hints:
|
||||
min_height_px: 350
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# Node-to-slot mapping contract for Phase Z-2.
|
||||
#
|
||||
# This file is introduced by #98 T21.6 as a contract artifact. T25 will wire
|
||||
# builders/mappers to validate that normalized source nodes have an allowed
|
||||
# frame slot target before render.
|
||||
|
||||
version: 1
|
||||
|
||||
coverage_states:
|
||||
covered_native:
|
||||
description: Existing frame and builder can render all required source atoms verbatim.
|
||||
ai_allowed: false
|
||||
covered_via_expand:
|
||||
description: Existing frame can render after deterministic repeat/expand without shrinking font size.
|
||||
ai_allowed: false
|
||||
requires_adaptation:
|
||||
description: Visual motif is usable, but adapted slot schema/repeat structure is needed.
|
||||
ai_allowed: true
|
||||
ai_text_generation_allowed: false
|
||||
unsupported:
|
||||
description: No current contract path exists.
|
||||
ai_allowed: false
|
||||
|
||||
coverage_state_resolution:
|
||||
route_aware: true
|
||||
fallback_paths_override_candidate_status: true
|
||||
deterministic_expand_sets: covered_via_expand
|
||||
font_policy: no_font_shrink
|
||||
|
||||
node_types:
|
||||
pre_intro:
|
||||
allowed_slots: [title, label, body_text_lines, popup_body]
|
||||
default_unit_role: top_level_unit
|
||||
numbered_section:
|
||||
allowed_slots: [title, body_text_lines, image_target, table_row]
|
||||
default_unit_role: body_zone_unit
|
||||
subsection_decimal:
|
||||
allowed_slots: [label, body_text_lines, pillar, comparison_cell, table_row, internal_context]
|
||||
default_unit_role: frame_internal_group
|
||||
subsection_numeric:
|
||||
allowed_slots: [label, body_text_lines, pillar, internal_context]
|
||||
default_unit_role: frame_internal_group
|
||||
bullet_group:
|
||||
allowed_slots: [body_text_lines, comparison_cell, table_row]
|
||||
default_unit_role: source_atom_group
|
||||
table_like_content:
|
||||
allowed_slots: [table_row, comparison_cell, persona_column]
|
||||
default_unit_role: source_atom_group
|
||||
image_ref:
|
||||
allowed_slots: [image_target]
|
||||
default_unit_role: media_atom
|
||||
component_ref:
|
||||
allowed_slots: []
|
||||
default_unit_role: expand_before_matching
|
||||
details_popup:
|
||||
allowed_slots: [popup_body]
|
||||
default_unit_role: auxiliary_detail
|
||||
summary_footer:
|
||||
allowed_slots: [footer_summary]
|
||||
default_unit_role: fixed_base_slide_region
|
||||
|
||||
slot_types:
|
||||
title:
|
||||
text_source: unit_title_only
|
||||
label:
|
||||
text_source: child_heading_or_source_label_only
|
||||
body_text_lines:
|
||||
text_source: source_atoms_verbatim
|
||||
pillar:
|
||||
text_source: child_heading_plus_child_body
|
||||
persona_column:
|
||||
text_source: component_expanded_persona_node
|
||||
internal_context:
|
||||
text_source: child_heading_plus_child_body
|
||||
comparison_cell:
|
||||
text_source: source_table_cell_or_bullet_group
|
||||
table_row:
|
||||
text_source: source_row_or_key_value_atom
|
||||
image_target:
|
||||
text_source: source_image_ref
|
||||
popup_body:
|
||||
text_source: source_details_content
|
||||
footer_summary:
|
||||
text_source: summary_source_only
|
||||
@@ -57,6 +57,7 @@ slots: title, center_quote (옵션), quadrant_{1,2,3,4}_{label, body}
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
@@ -85,6 +86,10 @@ slots: title, center_quote (옵션), quadrant_{1,2,3,4}_{label, body}
|
||||
background: linear-gradient(0deg, #f2f2f2 0%, #ffffff 100%);
|
||||
border-radius: 0 0 14px 14px;
|
||||
}
|
||||
.f16b__quadrant--extra {
|
||||
background: linear-gradient(0deg, #f2f2f2 0%, #ffffff 100%);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* Header ribbon — PROMOTED gradient + token font */
|
||||
.f16b__ribbon {
|
||||
@@ -108,7 +113,8 @@ slots: title, center_quote (옵션), quadrant_{1,2,3,4}_{label, body}
|
||||
background: linear-gradient(270deg, rgba(165,161,150,0.5) -26.87%, #39321E 73.13%);
|
||||
}
|
||||
.f16b__quadrant--tr .f16b__ribbon,
|
||||
.f16b__quadrant--br .f16b__ribbon {
|
||||
.f16b__quadrant--br .f16b__ribbon,
|
||||
.f16b__quadrant--extra .f16b__ribbon {
|
||||
background: linear-gradient(90deg, rgba(41,107,85,0.5) 0%, #032118 100%);
|
||||
}
|
||||
|
||||
@@ -213,7 +219,27 @@ slots: title, center_quote (옵션), quadrant_{1,2,3,4}_{label, body}
|
||||
</div>
|
||||
|
||||
{# Center quote — 옵션 #}
|
||||
{% if slot_payload.center_quote %}
|
||||
{% set slot_count = slot_payload._slot_count or 4 %}
|
||||
{% if slot_count > 4 %}
|
||||
{% for n in range(5, slot_count + 1) %}
|
||||
{% set label = slot_payload['quadrant_' ~ n ~ '_label'] %}
|
||||
{% set body = slot_payload['quadrant_' ~ n ~ '_body'] %}
|
||||
{% set headline = slot_payload['quadrant_' ~ n ~ '_headline'] %}
|
||||
<div class="f16b__quadrant f16b__quadrant--extra" data-frame-slot-id="quadrant_dynamic" data-quadrant-n="{{ n }}">
|
||||
{% if label %}
|
||||
<div class="f16b__ribbon">{{ label | safe }}</div>
|
||||
{% endif %}
|
||||
{% if headline or body %}
|
||||
<div class="f16b__body">
|
||||
{% if headline %}<div class="f16b__headline">{{ headline | safe }}</div>{% endif %}
|
||||
{% for line in body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if slot_payload.center_quote and slot_count <= 4 %}
|
||||
<div class="f16b__center">
|
||||
<div class="f16b__center-quote">{{ slot_payload.center_quote | safe }}</div>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,6 @@ slots: title (omitted), banner_left, banner_right,
|
||||
}
|
||||
/* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override. */
|
||||
.f29b__cell .text-line--bullet { color: #000; font-weight: 700; }
|
||||
|
||||
/* AS-IS/TO-BE 좌우 + arrow (PROMOTED 구조) */
|
||||
.f29b__transform {
|
||||
display: grid;
|
||||
|
||||
@@ -80,13 +80,13 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
|
||||
.f13b__col--nature .f13b__bar { background: linear-gradient(180deg, #39BE49 21%, #23742C 100%); }
|
||||
/* PROMOTED Figma frame 13 line 92-93 — main + parenthetical stacked vertically */
|
||||
.f13b__bar-main {
|
||||
font-size: var(--font-sub-title); /* 12px — main word "기술/사람/자연" */
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
}
|
||||
.f13b__bar-paren {
|
||||
font-size: var(--font-caption); /* 10px — "(디지털/역량/여건)" */
|
||||
font-size: var(--font-caption);
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
@@ -96,29 +96,29 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
|
||||
.f13b__body {
|
||||
flex: 1;
|
||||
display: flex; flex-direction: column;
|
||||
padding: 8px 12px; /* breathing (was 6px 10px) */
|
||||
padding: 8px 12px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f13b__section {
|
||||
flex: 1;
|
||||
padding: 6px 0; /* breathing (was 4px) */
|
||||
padding: 6px 0;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
.f13b__section + .f13b__section {
|
||||
border-top: 2px dashed #000; /* PROMOTED line 103 */
|
||||
margin-top: 6px; /* visible section gap (was 4px) */
|
||||
padding-top: 8px; /* breathing (was 6px) */
|
||||
margin-top: 6px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
/* heading — 부소목차 (사용자 5-layer 직설 2026-05-07): Digital 기술과... 같은 깊이 1 굵은 bullet */
|
||||
.f13b__heading {
|
||||
font-size: var(--font-sub-title); /* 12px — 부소목차 위계 */
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: 700;
|
||||
line-height: var(--lh-sub-title);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
color: transparent;
|
||||
margin-bottom: 6px; /* visible heading-body gap (was 4px) */
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.f13b__col--tech .f13b__heading { background-image: linear-gradient(180deg, #0D78D0 0%, #134D7F 100%); }
|
||||
.f13b__col--people .f13b__heading { background-image: linear-gradient(180deg, #FE900C 0%, #D18B37 100%); }
|
||||
@@ -141,7 +141,6 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
|
||||
.f13b__desc {
|
||||
line-height: clamp(1.2em, calc(20cqh / var(--max-body-lines, 4)), 1.6em);
|
||||
}
|
||||
|
||||
/* IMP-36 (Gitea #65 u4) P1 — aspect-ratio < 1.5 rotation rule. zone 의
|
||||
가로:세로 비가 1.5 미만으로 좁아지면 (vertical-2 narrow / 또는 임의
|
||||
세로형 geometry) 3-col grid 가 1-col stack 으로 회전. */
|
||||
|
||||
@@ -90,10 +90,40 @@ Asset path runtime resolution :
|
||||
}
|
||||
.f14b__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f14b__intro {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 6px;
|
||||
max-height: 76px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f14b__intro-card {
|
||||
border: 1px solid rgba(40, 91, 74, 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
padding: 5px 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f14b__intro-label {
|
||||
font-size: var(--font-caption);
|
||||
line-height: var(--lh-caption);
|
||||
font-weight: 700;
|
||||
color: #285b4a;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.f14b__intro-body .text-line {
|
||||
font-size: var(--font-caption);
|
||||
line-height: var(--lh-caption);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.f14b__col {
|
||||
position: relative;
|
||||
@@ -223,10 +253,36 @@ Asset path runtime resolution :
|
||||
opacity: 0.7; /* index.html L53 — opacity 0.70 */
|
||||
display: block;
|
||||
}
|
||||
.f14b--with-intro .f14b__intro { max-height: 56px; }
|
||||
.f14b--with-intro .f14b__badge {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin: 3px auto 2px;
|
||||
}
|
||||
.f14b--with-intro .f14b__body {
|
||||
padding: 2px 8px 5px;
|
||||
}
|
||||
.f14b--with-intro .f14b__photo {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="f14b" data-frame-id="1171281191" data-template-id="three_persona_benefits">
|
||||
<div class="f14b{% if slot_payload.intro_sections %} f14b--with-intro{% endif %}" data-frame-id="1171281191" data-template-id="three_persona_benefits">
|
||||
<div class="f14b__title">{{ slot_payload.title }}</div>
|
||||
{% if slot_payload.intro_sections %}
|
||||
<div class="f14b__intro">
|
||||
{% for section in slot_payload.intro_sections %}
|
||||
<div class="f14b__intro-card">
|
||||
{% if section.label %}<div class="f14b__intro-label">{{ section.label | safe }}</div>{% endif %}
|
||||
{% if section.body %}
|
||||
<div class="f14b__intro-body">
|
||||
{% for line in section.body %}<div class="text-line{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="f14b__cols">
|
||||
{% for persona in slot_payload.personas %}
|
||||
<div class="f14b__col f14b__col--{{ persona.color_class }}">
|
||||
@@ -258,11 +314,11 @@ Asset path runtime resolution :
|
||||
{# bottom photo — per persona 실사 사진 #}
|
||||
<div class="f14b__photo">
|
||||
{% if persona.color_class == "client" %}
|
||||
<img src="assets/three_persona_benefits/d2c070f200af83f563976b6b0f309d38321d204d.png" alt="">
|
||||
<img src="assets/three_persona_benefits/d2c070f200af83f563976b6b0f309d38321d204d.png" alt="" data-allow-aspect-crop="1">
|
||||
{% elif persona.color_class == "constructor" %}
|
||||
<img src="assets/three_persona_benefits/2a6a58e7bf7a645b5ede65115feb2890ccc414d1.png" alt="">
|
||||
<img src="assets/three_persona_benefits/2a6a58e7bf7a645b5ede65115feb2890ccc414d1.png" alt="" data-allow-aspect-crop="1">
|
||||
{% elif persona.color_class == "designer" %}
|
||||
<img src="assets/three_persona_benefits/39113493f6e3ae76d766e86e293b0f0dcbf55d91.png" alt="">
|
||||
<img src="assets/three_persona_benefits/39113493f6e3ae76d766e86e293b0f0dcbf55d91.png" alt="" data-allow-aspect-crop="1">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<!-- Phase Z-2 MVP-1 scaffold — MVP-2 이후 정식 Family CSS partial 로 대체. legacy structures/* 는 stylization 참고만. -->
|
||||
{# Frame 29 (1171281210) / process_product_two_way
|
||||
slots: title, banner_{left, right}, process/product (semantic naming)
|
||||
family: compare / layout: banner-top-2col-bottom / cardinality: 2
|
||||
시각 언어 (analysis.md + preview.png 참고 — frame 29 미변환이라 한계) :
|
||||
- 상단 배너 2 개 : 좌(과정) 우(결과)
|
||||
- 하단 2 col : 각각 변환 카드 (AS-IS → TO-BE) 또는 bullets
|
||||
MVP-1 한계 명시 :
|
||||
- frame 29 는 figma_to_html_agent 1:1 변환 미존재 (analysis 만)
|
||||
- banner 그라데이션 / cell 색은 frame 13 의 시각 언어와 일관 (blue 기반) #}
|
||||
|
||||
<style scoped>
|
||||
.frame-pp2w {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.frame-pp2w__title {
|
||||
font-size: var(--font-zone-title);
|
||||
font-weight: var(--weight-zone-title);
|
||||
line-height: var(--lh-zone-title);
|
||||
color: #1e293b;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.frame-pp2w__banners {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
.frame-pp2w__banner {
|
||||
padding: 4px 10px;
|
||||
background: linear-gradient(90deg, #1e40af 0%, #2563eb 100%);
|
||||
color: #fff;
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: var(--weight-sub-title);
|
||||
line-height: var(--lh-sub-title);
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.frame-pp2w__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.frame-pp2w__col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* AS-IS → TO-BE transform 카드 */
|
||||
.frame-pp2w__transform {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
font-size: var(--font-caption);
|
||||
font-weight: var(--weight-caption);
|
||||
line-height: var(--lh-caption);
|
||||
}
|
||||
.frame-pp2w__cell {
|
||||
padding: 3px 6px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
color: #475569;
|
||||
overflow: hidden;
|
||||
}
|
||||
.frame-pp2w__cell--to {
|
||||
background: #dbeafe;
|
||||
border-color: #93c5fd;
|
||||
color: #1e40af;
|
||||
font-weight: 700;
|
||||
}
|
||||
.frame-pp2w__arrow {
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
font-size: var(--font-body);
|
||||
}
|
||||
/* bullets (transforms 없거나 추가 항목) */
|
||||
.frame-pp2w__bullets {
|
||||
font-size: var(--font-caption);
|
||||
font-weight: var(--weight-caption);
|
||||
line-height: var(--lh-caption);
|
||||
color: #475569;
|
||||
overflow: hidden;
|
||||
}
|
||||
.frame-pp2w__bullets ul { padding-left: 12px; margin-top: 2px; }
|
||||
.frame-pp2w__bullets li { margin-bottom: 2px; }
|
||||
.frame-pp2w__bullets strong { color: #1e293b; font-weight: 700; }
|
||||
</style>
|
||||
|
||||
<div class="frame-pp2w" data-frame-id="1171281210" data-template-id="process_product_two_way">
|
||||
<div class="frame-pp2w__title">{{ slot_payload.title }}</div>
|
||||
<div class="frame-pp2w__banners">
|
||||
<div class="frame-pp2w__banner">{{ slot_payload.banner_left }}</div>
|
||||
<div class="frame-pp2w__banner">{{ slot_payload.banner_right }}</div>
|
||||
</div>
|
||||
<div class="frame-pp2w__cols">
|
||||
<div class="frame-pp2w__col" data-col="process">
|
||||
{% if slot_payload.process.transforms %}
|
||||
{% for t in slot_payload.process.transforms %}
|
||||
<div class="frame-pp2w__transform">
|
||||
<div class="frame-pp2w__cell">{{ t.from }}</div>
|
||||
<div class="frame-pp2w__arrow">➜</div>
|
||||
<div class="frame-pp2w__cell frame-pp2w__cell--to">{{ t.to }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if slot_payload.process.bullets %}
|
||||
<div class="frame-pp2w__bullets">{{ slot_payload.process.bullets | safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="frame-pp2w__col" data-col="product">
|
||||
{% if slot_payload.product.transforms %}
|
||||
{% for t in slot_payload.product.transforms %}
|
||||
<div class="frame-pp2w__transform">
|
||||
<div class="frame-pp2w__cell">{{ t.from }}</div>
|
||||
<div class="frame-pp2w__arrow">➜</div>
|
||||
<div class="frame-pp2w__cell frame-pp2w__cell--to">{{ t.to }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if slot_payload.product.bullets %}
|
||||
<div class="frame-pp2w__bullets">{{ slot_payload.product.bullets | safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,115 @@
|
||||
<!-- Phase Z-2 MVP-1 scaffold — MVP-2 이후 정식 Family CSS partial 로 대체. legacy structures/* 는 stylization 참고만. -->
|
||||
{# Frame 13 (1171281190) / three_parallel_requirements
|
||||
slots: title, pillar_{1,2,3}_{label, body}
|
||||
family: list / layout: 3-column / cardinality: 3
|
||||
시각 언어 도출 (figma_to_html_agent/blocks/1171281190/index.html 참고) :
|
||||
- 3 columns + 각 좌측 color bar (gradient)
|
||||
- 색상 : 기술 blue / 사람 orange / 자연 green (Figma 색)
|
||||
- 각 col 2 sections (heading + desc) — MVP-1 은 단일 body 로 단순화
|
||||
- top/bottom 검정 border (col 외곽)
|
||||
MVP-1 단순화 :
|
||||
- zoom 안 씀 — 기존 px 직접 (compact scale)
|
||||
- 한자 (技/人/天) 안 씀 — 라벨만 #}
|
||||
|
||||
<style scoped>
|
||||
.frame-3par {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.frame-3par__title {
|
||||
font-size: var(--font-zone-title);
|
||||
font-weight: var(--weight-zone-title);
|
||||
line-height: var(--lh-zone-title);
|
||||
color: #1e293b;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.frame-3par__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.frame-3par__pillar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1.5px solid #1e293b;
|
||||
border-bottom: 1.5px solid #1e293b;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 좌측 color bar (Figma 색상 도출) */
|
||||
.frame-3par__bar {
|
||||
flex: 0 0 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: var(--font-caption);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
padding: 4px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* 3 색 — Figma 추출값 */
|
||||
.frame-3par__pillar:nth-child(1) .frame-3par__bar {
|
||||
background: linear-gradient(180deg, #0D78D0 0%, #023056 100%);
|
||||
}
|
||||
.frame-3par__pillar:nth-child(2) .frame-3par__bar {
|
||||
background: linear-gradient(180deg, #FF9A23 0%, #CC5200 100%);
|
||||
}
|
||||
.frame-3par__pillar:nth-child(3) .frame-3par__bar {
|
||||
background: linear-gradient(180deg, #39BE49 21%, #23742C 100%);
|
||||
}
|
||||
.frame-3par__body-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.frame-3par__label {
|
||||
font-size: var(--font-sub-title);
|
||||
font-weight: var(--weight-sub-title);
|
||||
line-height: var(--lh-sub-title);
|
||||
color: #1e293b;
|
||||
padding-bottom: 3px;
|
||||
border-bottom: 1px dashed #94a3b8;
|
||||
}
|
||||
.frame-3par__pillar:nth-child(1) .frame-3par__label { color: #134D7F; }
|
||||
.frame-3par__pillar:nth-child(2) .frame-3par__label { color: #D18B37; }
|
||||
.frame-3par__pillar:nth-child(3) .frame-3par__label { color: #23742C; }
|
||||
|
||||
.frame-3par__body {
|
||||
font-size: var(--font-body);
|
||||
font-weight: var(--weight-body);
|
||||
line-height: var(--lh-body);
|
||||
color: #475569;
|
||||
overflow: hidden;
|
||||
}
|
||||
.frame-3par__body ul { padding-left: 14px; margin-top: 2px; }
|
||||
.frame-3par__body li { margin-bottom: 2px; }
|
||||
.frame-3par__body strong { color: #1e293b; font-weight: 700; }
|
||||
</style>
|
||||
|
||||
<div class="frame-3par" data-frame-id="1171281190" data-template-id="three_parallel_requirements">
|
||||
<div class="frame-3par__title">{{ slot_payload.title }}</div>
|
||||
<div class="frame-3par__cols">
|
||||
{% for pillar in slot_payload.pillars %}
|
||||
<div class="frame-3par__pillar">
|
||||
<div class="frame-3par__bar">{{ pillar.label }}</div>
|
||||
<div class="frame-3par__body-wrap">
|
||||
<div class="frame-3par__label">{{ pillar.label }}</div>
|
||||
<div class="frame-3par__body">{{ pillar.body | safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,9 +27,12 @@
|
||||
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||
background: #e8ecf0;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
word-break: keep-all;
|
||||
padding: 20px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* ── IMP-14 A-4: embedded mode reset (iframe consumer) ──
|
||||
@@ -360,7 +363,9 @@
|
||||
<body>
|
||||
<div class="slide" data-page="1">
|
||||
<div class="slide-bg"></div>
|
||||
{% if show_debug_marker %}
|
||||
<div class="phase-z2-marker">phase_z2 / mvp-1.5b / {{ layout_preset }} / single slide</div>
|
||||
{% endif %}
|
||||
<div class="slide-title">{{ slide_title }}</div>
|
||||
<div class="slide-divider"></div>
|
||||
<div class="slide-body">
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# PIPELINE — V1 ~ V4 매칭 시스템 통합 정리
|
||||
|
||||
이 문서는 MDX ↔ Figma Frame 매칭 시스템의 V1 ~ V4 단계를 한 곳에 정리한다.
|
||||
새 작업자 (또는 새 Claude 세션) 가 이 문서만 읽어도 시스템 전체를 파악할 수 있도록 작성.
|
||||
|
||||
---
|
||||
|
||||
## 사전 작업 (V1 이전)
|
||||
|
||||
### S.1 입력 수집
|
||||
|
||||
| 출처 | 입력 문서 | 수집 문장 |
|
||||
|---|---|---|
|
||||
| Figma 프레임 텍스트 | 32 개 | 586 |
|
||||
| BEPS 마스터 텍스트 | 1 개 | 1,410 |
|
||||
| MDX 검증 구간 | 4 개 | 129 |
|
||||
| **합계** | **37** | **2,125** |
|
||||
|
||||
- **코드** : `pipeline_01_extract_nodes.py`
|
||||
- **결과** : `actual_text_nodes.yaml`
|
||||
|
||||
### S.2 키워드 추출 + 정리
|
||||
|
||||
```
|
||||
2,125 문장 → 형태소 분석 (Kiwi) → 13,913 등장 / 1,738 고유
|
||||
↓ 표기 통합 + 보존 규칙
|
||||
전처리 후 14,161 등장 / 1,713 고유
|
||||
↓ 등장 범위 분류
|
||||
핵심 451 + 연관 1,238 + 광범위 24 = 1,713
|
||||
추가: 키워드 세트 524 (frame-level 시그니처)
|
||||
```
|
||||
|
||||
**적용 규칙**
|
||||
- 중요 표기 보존 13개: `DX`, `BIM`, `S/W`, `H/W`, `2D`, `3D`, `AS-IS`, `TO-BE` 등
|
||||
- 표기 통합: "디지털 전환" / "디지털전환" → 대표 표기 `DX`
|
||||
- 제외: 1글자 단어, 순수 숫자, 공백/기호만 있는 토큰
|
||||
- 품사: 일반명사 / 고유명사 / 외국어 / 숫자만
|
||||
|
||||
**코드** : `pipeline_02_tokenize.py`, `pipeline_04_normalize.py`, `pipeline_05_anchor_candidates.py`, `pipeline_07_auto_anchor_candidates.py`
|
||||
**결과** : `auto_anchor_candidates.yaml`, `keyword_base.yaml`, `domain_terms.yaml`
|
||||
|
||||
### S.3 Frame 구조 라벨링 (templates_v2 ontology)
|
||||
|
||||
32 개 Figma 프레임 각각에 다음 정보 라벨링:
|
||||
- `visual_pattern.layout` (compare-rows / table-2col / cards-3col 등)
|
||||
- `visual_pattern.family` (table / card / diagram / list)
|
||||
- `visual_pattern.relation_type` (compare / parallel / sequence)
|
||||
- `visual_pattern.cardinality` (ideal / min / max)
|
||||
- `content_affinity.primary` + `secondary` (12 카테고리)
|
||||
- `structure_intent_v2.primary` + `secondary` (9 카테고리)
|
||||
- `slots` (146 고유 slot id — title, col_a_label, col_b_label, rows 등)
|
||||
- `alternative_patterns` (대안 layout)
|
||||
|
||||
**도출 과정** : r1 → r2 → r3 → final → final_r2 (수동 오버라이드 3건)
|
||||
**코드** : `pipeline_12_generate_templates_v2.py`, `pipeline_12_r3_generate_templates_v2.py`, `pipeline_12_finalize_v2.py`
|
||||
**결과** : `structure_ontology_v2_final_r2.yaml`
|
||||
|
||||
---
|
||||
|
||||
## V1 — 키워드 매칭
|
||||
|
||||
### 무엇을 보는가
|
||||
MDX 본문 단어와 frame 의 3 계층 키워드 (핵심 / 세트 / 연관) 의 일치 비율.
|
||||
|
||||
### 산식
|
||||
```
|
||||
matching_score = 0.414 × 핵심 점수 + 0.320 × 세트 점수 + 0.265 × 연관 점수
|
||||
```
|
||||
- 핵심 점수 = (frame 핵심 키워드 중 MDX 본문에 등장한 비율)
|
||||
- 세트 점수 = (frame 세트들의 MDX 매칭 coverage 평균)
|
||||
- 연관 점수 = (frame 연관 키워드 중 MDX 본문에 등장한 비율)
|
||||
|
||||
### 가중치 도출 과정
|
||||
1. 초기 (수동) : 0.30 / 0.50 / 0.20 — 도메인 판단
|
||||
2. **Logistic Regression 학습** (L2, C=1.0, scikit-learn)
|
||||
- 데이터: TARGET 4 × 32 frame = 128 샘플 (정답 4 / 오답 124)
|
||||
- Feature: (standalone_score, group_score, related_score)
|
||||
- Label: 1 (정답) / 0 (오답)
|
||||
- 학습 결과: **0.414 / 0.320 / 0.265**
|
||||
3. **검증 — LOOCV** (Leave-One-Out Cross-Validation)
|
||||
- 4 TARGET 각각 hold-out → 나머지 3 개로 학습 → hold-out 정답 판별
|
||||
- 결과: **4 / 4 모두 정답**
|
||||
- 가중치 변동: 0.40 ± 0.02 / 0.32 ± 0.02 / 0.27 ± 0.01 (안정적)
|
||||
|
||||
### 결과 정확도
|
||||
| 방식 | TARGET 정답 |
|
||||
|---|---|
|
||||
| 현재 (수동 0.30/0.50/0.20) | 4 / 4 |
|
||||
| **Logistic Regression (0.414/0.320/0.265)** | **4 / 4** |
|
||||
| OLS Linear (1.0/0.05/-0.05) | 3 / 4 |
|
||||
| BM25 (개별 토큰) | 3 / 4 |
|
||||
| BM25 (세트 포함) | 3 / 4 |
|
||||
| IDF-weighted | 3 / 4 |
|
||||
|
||||
### 발견된 약점
|
||||
- 02-2.2 (TARGET) : 정답 Frame 14 의 anchor 키워드가 MDX 본문과 거의 안 겹침 → V1 점수 낮음 (V4 까지 가도 reject)
|
||||
|
||||
### 코드 + 결과
|
||||
- 매칭 코드 : `pipeline_06_2_mdx_matching.py`
|
||||
- 가중치 학습 : `pipeline_15_logistic_regression.py`
|
||||
- 결과 비교 : `pipeline_15_bm25_with_sets.py`, `pipeline_15_idf_comparison.py`
|
||||
- 결과 파일 : `mdx_matching_result.yaml`
|
||||
- 검증 보고서 : `ATTACH_07_WEIGHT_VALIDATION.html`
|
||||
|
||||
---
|
||||
|
||||
## V2 — 의미 매칭 (semantic rerank)
|
||||
|
||||
### 무엇을 보는가
|
||||
"키워드는 다르지만 의미가 같은가?" — MDX summary 와 frame.content 의 의미적 유사도.
|
||||
|
||||
### 산식
|
||||
```
|
||||
similarity = cosine(embed(MDX summary), embed(frame.content))
|
||||
```
|
||||
- 모델: `jhgan/ko-sroberta-multitask` (한국어 Sentence-BERT)
|
||||
|
||||
### 도출 과정
|
||||
- 별도 가중치 학습 없음. cosine 유사도 그대로 사용.
|
||||
- V1 Top-K 후보 안에서 의미 유사도로 재정렬.
|
||||
|
||||
### 결과 정확도
|
||||
- V1 후보 재정렬 단계라 단독 정확도는 별도 측정 X
|
||||
- V3, V4 의 출발점 (V2 Top-K 후보 → V3 평가)
|
||||
|
||||
### 발견된 약점
|
||||
- 의미 임베딩 단독으로는 frame 식별력 부족 (도메인 키워드 신호가 더 강함)
|
||||
- V1 Top-K 가 부정확하면 V2 도 그 한계 안에서 작동
|
||||
|
||||
### 코드 + 결과
|
||||
- 매칭 코드 : `pipeline_08_v2_semantic_rerank.py`
|
||||
- 임베딩 모듈 : `embeddings.py`
|
||||
- 결과 파일 : `v2_semantic_rerank_result.yaml`
|
||||
- 진단 : `pipeline_09_v2_diagnosis.py`, `V2_DIAGNOSIS.md`
|
||||
|
||||
---
|
||||
|
||||
## V3 — 구조 매칭 (structure rerank)
|
||||
|
||||
### 무엇을 보는가
|
||||
"이 콘텐츠를 담을 수 있는 레이아웃인가?" — MDX 글 구조와 frame 의 구조 속성 비교.
|
||||
|
||||
### 산식
|
||||
```
|
||||
구조 점수 = 0.40 × 레이아웃 일치 + 0.35 × 콘텐츠 성격 일치 + 0.25 × 시각 의도 일치
|
||||
```
|
||||
|
||||
**구성 요소**
|
||||
- **레이아웃 일치** : MDX layout family (table/card/diagram/list) ↔ frame family
|
||||
- **콘텐츠 성격** : 12 카테고리 키워드 사전 매칭 (concept_definition / comparative_matrix / policy_requirements 등)
|
||||
- **시각 의도** : 9 카테고리 키워드 사전 매칭 (matrix_coverage / cycle_interrelation / state_transition 등)
|
||||
|
||||
### MDX 본문 분석 방법
|
||||
- **레이아웃 자동 감지** : `detect_mdx_layout_v2(text)` — 표 / 불릿 / 헤딩 패턴 → layout enum → family
|
||||
- **콘텐츠 성격** : `AFFINITY_KEYWORDS` 사전 매칭 → 등장 횟수 1위 카테고리 = primary
|
||||
- **시각 의도** : `INTENT_KEYWORDS` 사전 매칭 + STRONG 구절 패턴 검증 (예: state_transition 은 "AS-IS / TO-BE" 1개 이상 필요)
|
||||
|
||||
### 도출 과정 (5회 튜닝 r1 → r5)
|
||||
| 라운드 | 변경 내용 | 결과 |
|
||||
|---|---|---|
|
||||
| r1 | 초기 키워드 사전 (12 + 9 카테고리) | TARGET 2/4 |
|
||||
| r2 | layout_compat 가중치 조정 | TARGET 2/4 |
|
||||
| r3 | STRONG 패턴 검증 도입 (before_after_change, state_transition) | TARGET 3/4 |
|
||||
| r4 | concept_definition 에서 "개념" 제거, hierarchy 단일 키워드 금지 | TARGET 3/4 |
|
||||
| **r5** | ontology final_r2 (Frame 13 review_queue 해제) | **TARGET 4/4** |
|
||||
|
||||
### 결과 정확도
|
||||
- TARGET : **4/4** (V2 Top-5 후보 안에서)
|
||||
- 단, V2 후보가 부정확하면 V3 도 부정확
|
||||
|
||||
### 발견된 약점
|
||||
- **콘텐츠 성격 / 시각 의도 분류 부정확** : 키워드 사전 매칭이라 단순 카운트
|
||||
- 예: MDX 01-2 (DX 와 BIM 의 구분) → 본문에 "국내, 도입, 사례" 가 많아 `policy_requirements` 1위 (실제 `comparative_matrix`)
|
||||
- 한계: 도메인 키워드 사전 의존, 일반 표현 못 잡음
|
||||
|
||||
### 코드 + 결과
|
||||
- 매칭 코드 : `pipeline_08_v3_r5_structure_rerank.py` (최종)
|
||||
- 의존 모듈 : `pipeline_08_v3_r4_structure_rerank.py`, `pipeline_08_v3_r2_structure_rerank.py`, `structure_v3.py`, `phase_common.py`
|
||||
- MDX 분석 : `detect_mdx.py`
|
||||
- 결과 파일 : `v3_structure_rerank_r5_result.yaml`
|
||||
|
||||
---
|
||||
|
||||
## V4 — 종합 판정 (template-fit-v1)
|
||||
|
||||
### 무엇을 보는가
|
||||
"실제로 쓸 수 있나? 어느 정도 수정해야 하나?" — V1/V2/V3 신호를 합쳐 frame 적용 가능성 판정.
|
||||
|
||||
### 산식
|
||||
```
|
||||
base = 0.25 × anchor + 0.20 × cardinality + 0.20 × relation
|
||||
+ 0.15 × slot + 0.20 × content
|
||||
|
||||
confidence = max(0, base − penalty)
|
||||
```
|
||||
|
||||
**5 축 의미**
|
||||
- `anchor` (0.25) : MDX detected_terms ↔ frame anchor_sets 매칭 (V1 핵심 키워드 매칭과 유사)
|
||||
- `cardinality` (0.20) : MDX 항목 수가 frame cardinality [min, max] 범위 안인가
|
||||
- `relation` (0.20) : MDX relation_type (compare/parallel/sequence) ↔ frame relation_type 일치
|
||||
- `slot` (0.15) : 휴리스틱 — within(0.5) + has_labels(0.3) + has_bodies(0.2)
|
||||
- `content` (0.20) : V2 의미 임베딩 cosine
|
||||
|
||||
**penalty**
|
||||
- cardinality mismatch 시 `adaptation_cost` 부과 (split / merge / infer_missing)
|
||||
- `not_suits` 패턴 매치 시 `not_suits_penalty`
|
||||
- 합계 capped at CAP_TOTAL
|
||||
|
||||
### 임계값 라우팅
|
||||
| 라벨 | confidence | 의미 | 후속 작업 |
|
||||
|---|---|---|---|
|
||||
| `use_as_is` | ≥ 0.90 | 그대로 사용 | 슬롯에 텍스트만 매핑 (자동) |
|
||||
| `light_edit` | ≥ 0.75 | 가벼운 편집 | AI 1 호출 (텍스트 다듬기) |
|
||||
| `restructure` | ≥ 0.60 | 구조 재배치 | 사람 + AI 여러 호출 |
|
||||
| `reject` | < 0.60 | 사용 불가 | 신규 디자인 |
|
||||
|
||||
### 도출 과정
|
||||
1. **r1 (V3 Top-K 평가만)** : V3 Top-5 안에서 confidence 산출 → 5개 중 선택
|
||||
2. **r2 (안정화)** : V2 후보 재선정 + adaptation_allowed 검증 추가
|
||||
3. **full32 (최종)** : 32 frame 전체 평가 → 진정한 reject 식별 가능
|
||||
|
||||
### 결과 정확도
|
||||
- TARGET : **3/4** (use_as_is 매칭 성공)
|
||||
- 01-2 ✓ (Frame 18, conf 0.946)
|
||||
- 02-2.2 ✗ (정답 Frame 14, V4 순위 7, reject)
|
||||
- 03-1 ✓ (Frame 13, conf 0.927)
|
||||
- 03-2 ✓ (Frame 29, conf 0.920)
|
||||
- Holdout : 사람 검수자 합리적 수준
|
||||
|
||||
### 발견된 약점
|
||||
|
||||
#### slot 축 ablation 결과
|
||||
- W_SLOT=0 으로 두고 4축 재정규화 → V4 재계산
|
||||
- **Top-1 매칭 7/7 동일** (모든 섹션)
|
||||
- TARGET Top-1 정답률: Baseline 3/4 → Ablated 3/4 (동일)
|
||||
- → **slot 축은 frame 선별에 영향 없음. 라벨 게이트 역할만 (use_as_is ↔ light_edit 임계 통과 여부)**
|
||||
|
||||
#### cardinality 중복 가중
|
||||
- cardinality 축 (0.20) 과 slot.within (0.075) 이 **같은 신호**를 두 번 평가
|
||||
- `item_count = len(slot_candidates)` 로 동일
|
||||
|
||||
#### 슬롯 의미 매핑 부재
|
||||
- frame 의 146 개 의미 slot id (`title`, `col_a_label`, `col_b_label`, `rows` 등) 가 V4 점수 계산에 활용 안 됨
|
||||
- "BIM → col_a_label" 같은 의미 매핑은 코드에 없음 — 후속 단계 책임
|
||||
|
||||
#### 표 첫 행만 추출
|
||||
- MDX 가 4 행 표여도 첫 행만 본문으로 추출. 나머지 행 (S/W·프로세스·성과품) 무시
|
||||
|
||||
### 코드 + 결과
|
||||
- 매칭 코드 : `pipeline_17_v4_full32.py` (최종, 32 frame 전체)
|
||||
- 의존 모듈 : `template_fit.py`
|
||||
- Ablation : `pipeline_18_slot_axis_ablation.py`
|
||||
- 결과 파일 : `v4_full32_result.yaml`
|
||||
- Ablation 보고서 : `V4_SLOT_ABLATION.md`
|
||||
|
||||
---
|
||||
|
||||
## 새 pipeline 통합 시 frame 단위 — 명확화
|
||||
|
||||
> **주의** : 이 매칭 시스템 (V1~V4) 은 **frame 32 개를 슬라이드 1장 단위로 검증** 함 (예: Frame 18 = BIM/DX 비교 슬라이드 통째로).
|
||||
>
|
||||
> 새 pipeline 의 위계는 **슬라이드 ⊃ slide-base ⊃ slide-body ⊃ 레이아웃 ⊃ Zone ⊃ 프레임** 이며, 여기서 **프레임 = zone 안 디자인 단위** 임.
|
||||
>
|
||||
> → frame DB 의 사이즈 라벨링 재검토 필요:
|
||||
> - 어떤 frame = 슬라이드 전체 (= 레이아웃 그 자체)
|
||||
> - 어떤 frame = zone 1 개 단위 (= 부분 디자인)
|
||||
> - 작은 박스 단위도 가능
|
||||
>
|
||||
> 새 pipeline 통합 작업의 사전 준비 항목 ([../IMPROVEMENT-REDESIGN.md](../../IMPROVEMENT-REDESIGN.md) 의 7.1 참조)
|
||||
|
||||
---
|
||||
|
||||
## 사후 작업 (V4 이후)
|
||||
|
||||
### 보고서 (DECK 1 ~ 7)
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `DECK_01_TARGET_01_2.html` | TARGET 01-2 × V1~V4 Top-3 매칭 결과 |
|
||||
| `DECK_02_HOLDOUT_02_2_1.html` | Holdout 02-2.1 × V1~V4 Top-3 |
|
||||
| `DECK_03_METHODS.html` | V1~V4 방법 설명 |
|
||||
| `DECK_04_DB_STRUCTURE.html` | MDX 1 vs Top-3 frame 5 컬럼 비교표 |
|
||||
| `DECK_05_STRUCTURE_KEYWORD.html` | 구조 매칭 4 항목 상세 (콘텐츠 성격 / 시각 의도 / 편집 슬롯 / 항목수·관계) |
|
||||
| `DECK_06_FIGMA_TO_DB.html` | Frame 18 의 DB 저장 형태 샘플 (YAML) |
|
||||
| `DECK_07_KEYWORD_PIPELINE.html` | 키워드집 정리 과정 + Frame 18 샘플 |
|
||||
|
||||
**생성 코드** : `pipeline_16_deck_4pages.py`
|
||||
|
||||
### 보조 보고서
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `KEYWORD_INVENTORY.html` | 키워드집 생성 보고서 (1,713 고유 + 13 중요 표기) |
|
||||
| `ATTACH_07_WEIGHT_VALIDATION.html` | V1 가중치 Logistic Regression 검증 |
|
||||
| `V4_SLOT_ABLATION.md` | V4 slot 축 ablation 결과 |
|
||||
| `FIGMA_KEYWORDS_REPORT.html` | Frame 별 키워드 인벤토리 |
|
||||
| `FRAME_KEYWORD_REVIEW.html` | Frame 별 키워드 검토 |
|
||||
|
||||
---
|
||||
|
||||
## 발견된 약점 종합 (8 가지)
|
||||
|
||||
| # | 약점 | 발견 시점 | 우선순위 |
|
||||
|---|---|---|---|
|
||||
| 1 | 02-2.2 매칭 실패 (정답 Frame 14 reject) | V4 결과 | 🥇 1순위 |
|
||||
| 2 | MDX 콘텐츠 성격 분류 부정확 (키워드 사전 한계) | V3 r5 검토 | 🥈 2순위 |
|
||||
| 3 | MDX 시각 의도 분류 부정확 (동일 한계) | V3 r5 검토 | 🥈 2순위 |
|
||||
| 4 | 편집 슬롯 의미 매핑 부재 | V4 코드 검토 | 🥉 3순위 |
|
||||
| 5 | V4 cardinality 중복 가중 | ablation | 🥉 3순위 |
|
||||
| 6 | V4 slot 축 frame 선별 무영향 | ablation | 🥉 3순위 (4 + 5 와 함께) |
|
||||
| 7 | 표 첫 행만 추출 (다행 무시) | DECK 04 정직성 점검 | 4순위 |
|
||||
| 8 | 그림/이미지 frame 매칭 약함 | V3 r5 검토 | 4순위 |
|
||||
|
||||
## 다음 단계 (개선 방향)
|
||||
|
||||
| 작업 | 내용 | 공수 |
|
||||
|---|---|---|
|
||||
| **02-2.2 교정** | Frame 14 anchor_sets 재라벨링 + 도메인 키워드 보강 → TARGET 4/4 | 1~2 일 |
|
||||
| **MDX 분석 LLM 화** | 콘텐츠 성격 / 시각 의도를 키워드 사전 → Anthropic API 호출 | 3~5 일 |
|
||||
| **슬롯 의미 매핑 모듈** | "BIM → col_a_label" 매핑 + V4 slot 축 진짜 역할 부여 | 5~7 일 |
|
||||
| **표 다행 처리** | rows 슬롯 채우기용 다행 추출 | 2~3 일 |
|
||||
| **그림 매칭 강화** | 본문 이미지 + 텍스트 결합 분석 | 2~3 일 |
|
||||
|
||||
---
|
||||
|
||||
## 핵심 메시지 (3 줄 요약)
|
||||
|
||||
1. **TARGET 75% (3/4)** — V1 가중치는 Logistic Regression 으로 검증 (LOOCV 4/4), BM25 / IDF 보다 우위
|
||||
2. **약점**: V3 의 콘텐츠 성격 / 시각 의도가 키워드 사전 기반이라 부정확. V4 의 slot 축은 ablation 으로 frame 선별 무영향 확인
|
||||
3. **다음 단계**: 02-2.2 교정 (1~2일) → MDX 분석 LLM 화 (3~5일) → 슬롯 의미 매핑 모듈 (5~7일)
|
||||