feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
@@ -0,0 +1,22 @@
# 조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다
## Alternative text
GET /feed를 받는 FeedController에서 GetFeedUseCase와 FeedQueryPort로 이어지고 FeedQueryAdapter가 포트를 구현하는 포트·어댑터 구조.
## Long description
왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.
## Elements and evidence
- **FeedController** (adapter): 조회 입력과 FeedSummary 반환 형태만 아는 웹 계층. Evidence: L323L324.
- **GetFeedUseCase** (application): 조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층. Evidence: L321L324.
- **FeedQueryPort** (interface): 상위 계층과 구체 조회 전략을 분리하는 조회 포트. Evidence: L323L325, L329L330.
- **FeedQueryAdapter** (adapter): FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터. Evidence: L323L325, L329L330.
## Relationships
- **FeedQueryAdapter → FeedQueryPort:** implements. Evidence: L324L325.
- **FeedController → GetFeedUseCase:** GET /feed 조회 위임. Evidence: L323L324.
- **GetFeedUseCase → FeedQueryPort:** 조회 의존. Evidence: L323L324.
@@ -0,0 +1,18 @@
# 조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다
# Question: GET /feed 조회는 어떤 상위 계층을 거쳐 포트에 도달하며, 실제 조회 전략은 어디에 격리되는가?
direction: right
n0: "FeedController" {
shape: rectangle
}
n1: "GetFeedUseCase" {
shape: rectangle
}
n2: "FeedQueryPort" {
shape: rectangle
}
n3: "FeedQueryAdapter" {
shape: rectangle
}
n0 -> n1: "GET /feed 조회 위임"
n1 -> n2: "조회 의존"
n3 -> n2: "implements"
@@ -0,0 +1,12 @@
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="FeedController", shape=box, style="rounded,filled"];
n1 [label="GetFeedUseCase", shape=box, style="rounded,filled"];
n2 [label="FeedQueryPort", shape=box, style="rounded,filled"];
n3 [label="FeedQueryAdapter", shape=box, style="rounded,filled"];
n0 -> n1 [label="GET /feed 조회 위임", style=solid];
n1 -> n2 [label="조회 의존", style=solid];
n3 -> n2 [label="implements", style=solid];
}
@@ -5,16 +5,16 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_get-feed-use-case" value="GetFeedUseCase" tooltip="조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=hexagon;perimeter=hexagonPerimeter2;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxCell id="n_get-feed-use-case" value="GetFeedUseCase" tooltip="조회 사용자, 페이지 크기, FeedSummary 계약만 아는 애플리케이션 계층. | Evidence: L321-L324" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=hexagon;perimeter=hexagonPerimeter2;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="420.0" y="185.0" width="230.0" height="130.0" as="geometry"/>
</mxCell>
<mxCell id="n_feed-controller" value="FeedController&lt;br/&gt;GET /feed" tooltip="조회 입력과 FeedSummary 반환 형태만 아는 웹 계층. | Evidence: L295-L295" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_feed-controller" value="FeedController&lt;br/&gt;GET /feed" tooltip="조회 입력과 FeedSummary 반환 형태만 아는 웹 계층. | Evidence: L323-L324" 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="85.0" width="210.0" height="94.0" as="geometry"/>
</mxCell>
<mxCell id="n_feed-query-adapter" value="FeedQueryAdapter&lt;br/&gt;PostgreSQL 조회&lt;br/&gt;Fetch Join · Batch Fetch&lt;br/&gt;DTO Projection · 윈도우 함수" tooltip="FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxCell id="n_feed-query-adapter" value="FeedQueryAdapter&lt;br/&gt;PostgreSQL 조회&lt;br/&gt;Fetch Join · Batch Fetch&lt;br/&gt;DTO Projection · 윈도우 함수" tooltip="FeedQueryPort를 구현하며 구체 조회 전략을 책임지는 퍼시스턴스 어댑터. | Evidence: L323-L325, L329-L330" 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="825.0" y="85.0" width="220.0" height="126.0" as="geometry"/>
</mxCell>
<mxCell id="n_feed-query-port" value="FeedQueryPort" tooltip="상위 계층과 구체 조회 전략을 분리하는 조회 포트. | Evidence: L295-L295, L299-L299" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rounded=1;arcSize=50;fillColor=#f6f1fb;" vertex="1" parent="1">
<mxCell id="n_feed-query-port" value="FeedQueryPort" tooltip="상위 계층과 구체 조회 전략을 분리하는 조회 포트. | Evidence: L323-L325, L329-L330" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rounded=1;arcSize=50;fillColor=#f6f1fb;" vertex="1" parent="1">
<mxGeometry x="670.0" y="155.0" width="125.0" height="58.0" as="geometry"/>
</mxCell>
<mxCell id="e_adapter-implements-port" value="implements" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_feed-query-adapter" target="n_feed-query-port">
@@ -0,0 +1,594 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "edge-adapter-implements-port",
"type": "arrow",
"x": 732.5,
"y": 211.0,
"width": 202.5,
"height": 48.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1777142792,
"version": 1,
"versionNonce": 210476259,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
202.5,
0.0
],
[
202.5,
48.0
],
[
0.0,
48.0
],
[
0.0,
2.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-feed-query-adapter",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-feed-query-port",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-adapter-implements-port",
"type": "text",
"x": 789.75,
"y": 219.0,
"width": 90,
"height": 24,
"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": 784346515,
"version": 1,
"versionNonce": 1186202715,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "implements",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "implements",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-controller-to-use-case",
"type": "arrow",
"x": 255.0,
"y": 132.0,
"width": 165.0,
"height": 118.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1068662408,
"version": 1,
"versionNonce": 213078762,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
82.5,
0.0
],
[
82.5,
118.0
],
[
165.0,
118.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-feed-controller",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-get-feed-use-case",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-controller-to-use-case",
"type": "text",
"x": 301.5,
"y": 179.0,
"width": 120,
"height": 24,
"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": 1964586448,
"version": 1,
"versionNonce": 1689984318,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "GET /feed 조회 위임",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "GET /feed 조회 위임",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-use-case-to-port",
"type": "arrow",
"x": 640.0,
"y": 119.0,
"width": 40.0,
"height": 131.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1760759207,
"version": 1,
"versionNonce": 39532668,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
10.0,
131.0
],
[
40.0,
131.0
],
[
40.0,
0.0
],
[
0.0,
0.0
],
[
0.0,
65.0
],
[
30.0,
65.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-get-feed-use-case",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-feed-query-port",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-use-case-to-port",
"type": "text",
"x": 659.0,
"y": 120.0,
"width": 90,
"height": 24,
"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": 1184230128,
"version": 1,
"versionNonce": 350650885,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "조회 의존",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "조회 의존",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-get-feed-use-case",
"type": "rectangle",
"x": 420.0,
"y": 185.0,
"width": 230.0,
"height": 130.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": 1013343996,
"version": 1,
"versionNonce": 1852011614,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-get-feed-use-case",
"type": "text",
"x": 430.0,
"y": 195.0,
"width": 210.0,
"height": 110.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": 692030406,
"version": 1,
"versionNonce": 302603492,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "GetFeedUseCase",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "GetFeedUseCase",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-feed-controller",
"type": "rectangle",
"x": 45.0,
"y": 85.0,
"width": 210.0,
"height": 94.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": 862823723,
"version": 1,
"versionNonce": 1007435196,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-feed-controller",
"type": "text",
"x": 55.0,
"y": 95.0,
"width": 190.0,
"height": 74.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": 1009068783,
"version": 1,
"versionNonce": 1697147702,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "FeedController\nGET /feed",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "FeedController\nGET /feed",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-feed-query-adapter",
"type": "rectangle",
"x": 825.0,
"y": 85.0,
"width": 220.0,
"height": 126.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": 1319244181,
"version": 1,
"versionNonce": 1104184560,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-feed-query-adapter",
"type": "text",
"x": 835.0,
"y": 95.0,
"width": 200.0,
"height": 106.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": 1895645289,
"version": 1,
"versionNonce": 873497168,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "FeedQueryAdapter\nPostgreSQL 조회\nFetch Join · Batch Fetch\nDTO Projection · 윈도우 함수",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "FeedQueryAdapter\nPostgreSQL 조회\nFetch Join · Batch Fetch\nDTO Projection · 윈도우 함수",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-feed-query-port",
"type": "rectangle",
"x": 670.0,
"y": 155.0,
"width": 125.0,
"height": 58.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": 1666315225,
"version": 1,
"versionNonce": 861140725,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-feed-query-port",
"type": "text",
"x": 680.0,
"y": 165.0,
"width": 105.0,
"height": 38.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": 692866467,
"version": 1,
"versionNonce": 250579912,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "FeedQueryPort",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "FeedQueryPort",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "query-port-boundary",
"spec_version": "1.1",
"spec_sha256": "7ca0fd6c2ed79efdd0ead574b3ad31e7c43d902d049c43e1f4884e9e8d0c1efe",
"source_context": {
"document": "docs/n+1liner/final/document.md",
"document_sha256": "f9e048a68db0ab82078955bf611b06a71a0033539e0c87ff0aa0d620a3126e36",
"anchor": {
"kind": "heading",
"value": "5.2 조회 전략은 포트 뒤 어댑터의 책임",
"line": 319
}
},
"outputs": [
"query-port-boundary.svg",
"query-port-boundary.drawio",
"query-port-boundary.mmd",
"query-port-boundary.d2",
"query-port-boundary.dot",
"query-port-boundary.excalidraw",
"query-port-boundary.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "ports-adapters",
"reference_ids": [
"order-ports-adapters"
],
"diagram_only": true
}
@@ -0,0 +1,10 @@
%% 조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다
%% question: GET /feed 조회는 어떤 상위 계층을 거쳐 포트에 도달하며, 실제 조회 전략은 어디에 격리되는가?
flowchart LR
n0["FeedController"]
n1["GetFeedUseCase"]
n2["FeedQueryPort"]
n3["FeedQueryAdapter"]
n0 -->|"GET /feed 조회 위임"| n1
n1 -->|"조회 의존"| n2
n3 -->|"implements"| n2
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1090" height="360" viewBox="0 0 1090 360" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">조회 전략은 FeedQueryPort 뒤의 퍼시스턴스 어댑터에 격리된다</title>
<desc id="diagram-description">왼쪽의 FeedController가 GET /feed 요청을 받아 중앙의 GetFeedUseCase에 조회를 위임한다. 유스케이스는 오른쪽의 FeedQueryPort에 조회를 의존한다. FeedQueryAdapter는 FeedQueryPort를 구현하는 아웃바운드 어댑터이며 PostgreSQL 조회를 수행한다. Fetch Join, Batch Fetch, DTO Projection, 윈도우 함수 같은 구체 전략은 이 어댑터의 책임이므로 상위 계층은 전략 교체의 영향을 받지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;query-port-boundary&quot;,&quot;profile&quot;:&quot;ports-adapters&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;/home/donghyeon/workspace/ai-tool/topic-arrange/n+1liner/n+1liner.md&quot;,&quot;document_sha256&quot;:&quot;1bb38964ca2a849690f5355646c1aa988111b4cd4e1055c6cb05cf7e5fc35cde&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;query-port-boundary&quot;,&quot;line&quot;:297}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;query-port-boundary&quot;,&quot;profile&quot;:&quot;ports-adapters&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/n+1liner/final/document.md&quot;,&quot;document_sha256&quot;:&quot;f9e048a68db0ab82078955bf611b06a71a0033539e0c87ff0aa0d620a3126e36&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;5.2 조회 전략은 포트 뒤 어댑터의 책임&quot;,&quot;line&quot;:319}},&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" />
@@ -50,29 +50,29 @@
</style>
</defs>
<rect class="canvas" width="1090" height="360" />
<polyline class="edge kind-dependency style-solid emphasis-normal" points="935.0,211.0 935.0,259.0 732.5,259.0 732.5,213.0" data-evidence="295-295" />
<polyline class="edge kind-dependency style-solid emphasis-normal" points="935.0,211.0 935.0,259.0 732.5,259.0 732.5,213.0" data-evidence="324-325" />
<rect class="edge-label-bg" x="792.2" y="217.0" width="85.0" height="22" rx="3" />
<text class="edge-label" x="834.8" y="232.0">implements</text>
<polyline class="edge kind-request style-solid emphasis-primary" points="255.0,132.0 337.5,132.0 337.5,250.0 420.0,250.0" data-evidence="295-295" />
<polyline class="edge kind-request style-solid emphasis-primary" points="255.0,132.0 337.5,132.0 337.5,250.0 420.0,250.0" data-evidence="323-324" />
<rect class="edge-label-bg" x="302.2" y="177.0" width="118.5" height="22" rx="3" />
<text class="edge-label" x="361.5" y="192.0">GET /feed 조회 위임</text>
<polyline class="edge kind-dependency style-solid emphasis-normal" points="650.0,250.0 680.0,250.0 680.0,119.0 640.0,119.0 640.0,184.0 670.0,184.0" data-evidence="295-295" />
<polyline class="edge kind-dependency style-solid emphasis-normal" points="650.0,250.0 680.0,250.0 680.0,119.0 640.0,119.0 640.0,184.0 670.0,184.0" data-evidence="323-324" />
<rect class="edge-label-bg" x="678.2" y="118.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="704.0" y="133.0">조회 의존</text>
<g id="node-get-feed-use-case">
<polygon class="node-shape kind-application emphasis-primary role-core" data-evidence="295-295" points="454.0,185.0 616.0,185.0 650.0,250.0 616.0,315.0 454.0,315.0 420.0,250.0" />
<polygon class="node-shape kind-application emphasis-primary role-core" data-evidence="321-324" points="454.0,185.0 616.0,185.0 650.0,250.0 616.0,315.0 454.0,315.0 420.0,250.0" />
<text class="node-role" x="535.0" y="204.0">«core»</text>
<text class="node-label" x="535.0" y="248.0">GetFeedUseCase</text>
</g>
<g id="node-feed-controller">
<rect class="node-shape kind-adapter emphasis-normal role-inbound-adapter" data-evidence="295-295" x="45.0" y="85.0" width="210.0" height="94.0" rx="7" />
<rect class="node-shape kind-adapter emphasis-normal role-inbound-adapter" data-evidence="323-324" x="45.0" y="85.0" width="210.0" height="94.0" rx="7" />
<text class="node-role" x="150.0" y="104.0">«inbound-adapter»</text>
<text class="node-label" x="150.0" y="131.0">FeedController</text>
<line class="node-detail-divider" x1="59.0" y1="152.0" x2="241.0" y2="152.0" />
<text class="node-detail" x="61.0" y="169.0">GET /feed</text>
</g>
<g id="node-feed-query-adapter">
<rect class="node-shape kind-adapter emphasis-normal role-outbound-adapter" data-evidence="295-295,299-299" x="825.0" y="85.0" width="220.0" height="126.0" rx="7" />
<rect class="node-shape kind-adapter emphasis-normal role-outbound-adapter" data-evidence="323-325,329-330" x="825.0" y="85.0" width="220.0" height="126.0" rx="7" />
<text class="node-role" x="935.0" y="104.0">«outbound-adapter»</text>
<text class="node-label" x="935.0" y="131.0">FeedQueryAdapter</text>
<line class="node-detail-divider" x1="839.0" y1="152.0" x2="1031.0" y2="152.0" />
@@ -81,7 +81,7 @@
<text class="node-detail" x="841.0" y="201.0">DTO Projection · 윈도우 함수</text>
</g>
<g id="node-feed-query-port">
<rect class="node-shape kind-interface emphasis-normal role-port" data-evidence="295-295,299-299" x="670.0" y="155.0" width="125.0" height="58.0" rx="22" />
<rect class="node-shape kind-interface emphasis-normal role-port" data-evidence="323-325,329-330" x="670.0" y="155.0" width="125.0" height="58.0" rx="22" />
<text class="node-role" x="732.5" y="174.0">«port»</text>
<text class="node-label" x="732.5" y="201.0">FeedQueryPort</text>
</g>

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB