feat: 문서 구조 변경 및 tech-visual 스킬 추가

This commit is contained in:
DongHyeonka
2026-09-04 18:20:00 +09:00
parent 43901f0abf
commit 2efb7ee1f2
683 changed files with 61180 additions and 10479 deletions
@@ -0,0 +1,16 @@
# 패턴 이동은 credential contract의 소유권 전환이다
## Alternative text
AP1에서 AP2, AP2에서 AP3, AP3에서 AP4, AP4에서 AP3로 이동할 때 호출 계약, 소유권, 브라우저 계약, 운영 책임과 전환 성격을 같은 다섯 축으로 비교한 네 항목.
## Long description
네 항목을 호출 계약, 소유권, 브라우저 계약, 운영 책임, 전환 성격의 다섯 축으로 비교한다. AP1에서 AP2는 Resource Server의 Bearer 계약을 유지하면서 OAuth callback을 mediator로 옮긴다. AP2에서 AP3는 browser의 access endpoint와 direct API 호출을 없애고 UI API를 BFF 계약으로 바꾼다. AP3에서 AP4는 application-owned session과 orchestration을 edge-owned session과 identity projection으로 전환한다. AP4에서 AP3로 되돌아가는 선택도 별도 항목으로 두어 번호 순서나 성숙도 상승을 암시하지 않는다.
## Elements and evidence
- **AP1 → AP2** (concept): Bearer API는 유지하되 OAuth callback과 browser-facing session·access endpoint의 책임을 mediator 쪽으로 옮기는 전환. Evidence: L1285L1285.
- **AP2 → AP3** (concept): Browser access handoff와 direct API 호출을 제거하고 UI의 API 계약과 관련 방어를 BFF가 소유하게 하는 전환. Evidence: L1287L1287.
- **AP3 → AP4** (concept): Application-owned session과 API orchestration을 edge-owned session과 identity projection으로 바꾸고 세밀한 인가를 재배치하는 전환. Evidence: L1289L1289.
- **AP4 → AP3** (concept): Upstream의 claim과 workflow 요구가 커질 때 header contract를 계속 넓히지 않고 책임을 BFF로 되돌리는 역방향 전환. Evidence: L1291L1291.
@@ -0,0 +1,15 @@
# 패턴 이동은 credential contract의 소유권 전환이다
# Question: AP1→AP2, AP2→AP3, AP3→AP4, AP4→AP3 이동에서 호출 계약과 책임 소유자는 각각 어떻게 바뀌는가?
direction: right
n0: "AP1 → AP2" {
shape: rectangle
}
n1: "AP2 → AP3" {
shape: rectangle
}
n2: "AP3 → AP4" {
shape: rectangle
}
n3: "AP4 → AP3" {
shape: rectangle
}
@@ -0,0 +1,9 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="AP1 → AP2", shape=box, style="rounded,filled"];
n1 [label="AP2 → AP3", shape=box, style="rounded,filled"];
n2 [label="AP3 → AP4", shape=box, style="rounded,filled"];
n3 [label="AP4 → AP3", shape=box, style="rounded,filled"];
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="credential-contract-migration" name="패턴 이동은 credential contract의 소유권 전환이다">
<mxGraphModel dx="1138" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1138" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_ap1-to-ap2" value="AP1 → AP2&lt;br/&gt;호출 · Bearer API 유지&lt;br/&gt;소유 · callback → mediator&lt;br/&gt;브라우저 · access API + session&lt;br/&gt;운영 · CORS → AP2 UI&lt;br/&gt;성격 · callback 재배치" tooltip="Bearer API는 유지하되 OAuth callback과 browser-facing session·access endpoint의 책임을 mediator 쪽으로 옮기는 전환. | Evidence: L1285-L1285" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="45.0" y="45.0" width="223.0" height="139.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap2-to-ap3" value="AP2 → AP3&lt;br/&gt;호출 · direct API → /bff/**&lt;br/&gt;소유 · UI API → BFF&lt;br/&gt;브라우저 · /token/access 제거&lt;br/&gt;운영 · error mapping + CSRF&lt;br/&gt;성격 · direct path 제거" tooltip="Browser access handoff와 direct API 호출을 제거하고 UI의 API 계약과 관련 방어를 BFF가 소유하게 하는 전환. | Evidence: L1287-L1287" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="320.0" y="45.0" width="223.0" height="139.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap3-to-ap4" value="AP3 → AP4&lt;br/&gt;호출 · API 조합 → identity&lt;br/&gt;소유 · app session → edge&lt;br/&gt;브라우저 · identity projection&lt;br/&gt;운영 · 인가 → upstream/policy&lt;br/&gt;성격 · upgrade 아님" tooltip="Application-owned session과 API orchestration을 edge-owned session과 identity projection으로 바꾸고 세밀한 인가를 재배치하는 전환. | Evidence: L1289-L1289" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="595.0" y="45.0" width="223.0" height="139.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap4-to-ap3" value="AP4 → AP3&lt;br/&gt;호출 · headers → BFF workflow&lt;br/&gt;소유 · claims/workflow → BFF&lt;br/&gt;브라우저 · app workflow&lt;br/&gt;운영 · state + verification&lt;br/&gt;성격 · 역방향 가능" tooltip="Upstream의 claim과 workflow 요구가 커질 때 header contract를 계속 넓히지 않고 책임을 BFF로 되돌리는 역방향 전환. | Evidence: L1291-L1291" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="870.0" y="45.0" width="223.0" height="139.0" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,289 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "node-ap1-to-ap2",
"type": "rectangle",
"x": 45.0,
"y": 45.0,
"width": 223.0,
"height": 139.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1557249645,
"version": 1,
"versionNonce": 294499605,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap1-to-ap2",
"type": "text",
"x": 55.0,
"y": 55.0,
"width": 203.0,
"height": 119.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1702265635,
"version": 1,
"versionNonce": 60175260,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP1 → AP2\n호출 · Bearer API 유지\n소유 · callback → mediator\n브라우저 · access API + session\n운영 · CORS → AP2 UI\n성격 · callback 재배치",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP1 → AP2\n호출 · Bearer API 유지\n소유 · callback → mediator\n브라우저 · access API + session\n운영 · CORS → AP2 UI\n성격 · callback 재배치",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap2-to-ap3",
"type": "rectangle",
"x": 320.0,
"y": 45.0,
"width": 223.0,
"height": 139.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1331377000,
"version": 1,
"versionNonce": 108260129,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap2-to-ap3",
"type": "text",
"x": 330.0,
"y": 55.0,
"width": 203.0,
"height": 119.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 770033263,
"version": 1,
"versionNonce": 1669588630,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP2 → AP3\n호출 · direct API → /bff/**\n소유 · UI API → BFF\n브라우저 · /token/access 제거\n운영 · error mapping + CSRF\n성격 · direct path 제거",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP2 → AP3\n호출 · direct API → /bff/**\n소유 · UI API → BFF\n브라우저 · /token/access 제거\n운영 · error mapping + CSRF\n성격 · direct path 제거",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap3-to-ap4",
"type": "rectangle",
"x": 595.0,
"y": 45.0,
"width": 223.0,
"height": 139.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 372334166,
"version": 1,
"versionNonce": 184792543,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap3-to-ap4",
"type": "text",
"x": 605.0,
"y": 55.0,
"width": 203.0,
"height": 119.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1382057023,
"version": 1,
"versionNonce": 693403148,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP3 → AP4\n호출 · API 조합 → identity\n소유 · app session → edge\n브라우저 · identity projection\n운영 · 인가 → upstream/policy\n성격 · upgrade 아님",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP3 → AP4\n호출 · API 조합 → identity\n소유 · app session → edge\n브라우저 · identity projection\n운영 · 인가 → upstream/policy\n성격 · upgrade 아님",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap4-to-ap3",
"type": "rectangle",
"x": 870.0,
"y": 45.0,
"width": 223.0,
"height": 139.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 91674431,
"version": 1,
"versionNonce": 1299751764,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap4-to-ap3",
"type": "text",
"x": 880.0,
"y": 55.0,
"width": 203.0,
"height": 119.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1921524377,
"version": 1,
"versionNonce": 81557497,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP4 → AP3\n호출 · headers → BFF workflow\n소유 · claims/workflow → BFF\n브라우저 · app workflow\n운영 · state + verification\n성격 · 역방향 가능",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP4 → AP3\n호출 · headers → BFF workflow\n소유 · claims/workflow → BFF\n브라우저 · app workflow\n운영 · state + verification\n성격 · 역방향 가능",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "credential-contract-migration",
"spec_version": "1.1",
"spec_sha256": "b075725369a92edd8b788f1c2bf6eb695cd07d0b78c3333ea8275631f3da1297",
"source_context": {
"document": "document.md",
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
"anchor": {
"kind": "marker",
"value": "credential-contract-migration",
"line": 1293
}
},
"outputs": [
"credential-contract-migration.svg",
"credential-contract-migration.drawio",
"credential-contract-migration.mmd",
"credential-contract-migration.d2",
"credential-contract-migration.dot",
"credential-contract-migration.excalidraw",
"credential-contract-migration.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "comparison",
"reference_ids": [
"contract-comparison"
],
"diagram_only": true
}
@@ -0,0 +1,7 @@
%% 패턴 이동은 credential contract의 소유권 전환이다
%% question: AP1→AP2, AP2→AP3, AP3→AP4, AP4→AP3 이동에서 호출 계약과 책임 소유자는 각각 어떻게 바뀌는가?
flowchart LR
n0["AP1 → AP2"]
n1["AP2 → AP3"]
n2["AP3 → AP4"]
n3["AP4 → AP3"]
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1138" height="300" viewBox="0 0 1138 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">패턴 이동은 credential contract의 소유권 전환이다</title>
<desc id="diagram-description">네 항목을 호출 계약, 소유권, 브라우저 계약, 운영 책임, 전환 성격의 다섯 축으로 비교한다. AP1에서 AP2는 Resource Server의 Bearer 계약을 유지하면서 OAuth callback을 mediator로 옮긴다. AP2에서 AP3는 browser의 access endpoint와 direct API 호출을 없애고 UI API를 BFF 계약으로 바꾼다. AP3에서 AP4는 application-owned session과 orchestration을 edge-owned session과 identity projection으로 전환한다. AP4에서 AP3로 되돌아가는 선택도 별도 항목으로 두어 번호 순서나 성숙도 상승을 암시하지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;credential-contract-migration&quot;,&quot;profile&quot;:&quot;comparison&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;credential-contract-migration&quot;,&quot;line&quot;:1293}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1138" height="300" />
<g id="node-ap1-to-ap2">
<rect class="node-shape kind-concept emphasis-normal role-contract" data-evidence="1285-1285" x="45.0" y="45.0" width="223.0" height="139.0" rx="7" />
<text class="node-label" x="156.5" y="72.5">AP1 → AP2</text>
<line class="node-detail-divider" x1="59.0" y1="93.5" x2="254.0" y2="93.5" />
<text class="node-detail" x="61.0" y="110.5">호출 · Bearer API 유지</text>
<text class="node-detail" x="61.0" y="126.5">소유 · callback → mediator</text>
<text class="node-detail" x="61.0" y="142.5">브라우저 · access API + session</text>
<text class="node-detail" x="61.0" y="158.5">운영 · CORS → AP2 UI</text>
<text class="node-detail" x="61.0" y="174.5">성격 · callback 재배치</text>
</g>
<g id="node-ap2-to-ap3">
<rect class="node-shape kind-concept emphasis-normal role-contract" data-evidence="1287-1287" x="320.0" y="45.0" width="223.0" height="139.0" rx="7" />
<text class="node-label" x="431.5" y="72.5">AP2 → AP3</text>
<line class="node-detail-divider" x1="334.0" y1="93.5" x2="529.0" y2="93.5" />
<text class="node-detail" x="336.0" y="110.5">호출 · direct API → /bff/**</text>
<text class="node-detail" x="336.0" y="126.5">소유 · UI API → BFF</text>
<text class="node-detail" x="336.0" y="142.5">브라우저 · /token/access 제거</text>
<text class="node-detail" x="336.0" y="158.5">운영 · error mapping + CSRF</text>
<text class="node-detail" x="336.0" y="174.5">성격 · direct path 제거</text>
</g>
<g id="node-ap3-to-ap4">
<rect class="node-shape kind-concept emphasis-normal role-contract" data-evidence="1289-1289" x="595.0" y="45.0" width="223.0" height="139.0" rx="7" />
<text class="node-label" x="706.5" y="72.5">AP3 → AP4</text>
<line class="node-detail-divider" x1="609.0" y1="93.5" x2="804.0" y2="93.5" />
<text class="node-detail" x="611.0" y="110.5">호출 · API 조합 → identity</text>
<text class="node-detail" x="611.0" y="126.5">소유 · app session → edge</text>
<text class="node-detail" x="611.0" y="142.5">브라우저 · identity projection</text>
<text class="node-detail" x="611.0" y="158.5">운영 · 인가 → upstream/policy</text>
<text class="node-detail" x="611.0" y="174.5">성격 · upgrade 아님</text>
</g>
<g id="node-ap4-to-ap3">
<rect class="node-shape kind-concept emphasis-normal role-contract" data-evidence="1291-1291" x="870.0" y="45.0" width="223.0" height="139.0" rx="7" />
<text class="node-label" x="981.5" y="72.5">AP4 → AP3</text>
<line class="node-detail-divider" x1="884.0" y1="93.5" x2="1079.0" y2="93.5" />
<text class="node-detail" x="886.0" y="110.5">호출 · headers → BFF workflow</text>
<text class="node-detail" x="886.0" y="126.5">소유 · claims/workflow → BFF</text>
<text class="node-detail" x="886.0" y="142.5">브라우저 · app workflow</text>
<text class="node-detail" x="886.0" y="158.5">운영 · state + verification</text>
<text class="node-detail" x="886.0" y="174.5">성격 · 역방향 가능</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB