wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규 - Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가 - templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신 - tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분) - tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가 - docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서 - scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸 - .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외 미완성 작업의 보존용 스냅샷 커밋 (2026-07-02) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,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>
|
||||
Binary file not shown.
|
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+122
@@ -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 됐지만 일부 콘텐츠 손실)."
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
Binary file not shown.
|
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>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"));
|
||||
|
||||
+12
-4
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user