feat: 가상화 문서들 추가
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
"groups": [
|
||||
{
|
||||
"id": "record-tables",
|
||||
"label": "기록은 종류마다 다른 테이블에 산다",
|
||||
"label": "종류별 테이블",
|
||||
"kind": "system",
|
||||
"role": "zone",
|
||||
"evidence": [
|
||||
@@ -53,7 +53,7 @@
|
||||
"shape": "box",
|
||||
"role": "source",
|
||||
"details": [
|
||||
"variant_label 로 축 이름을 정한다"
|
||||
"variant_label"
|
||||
],
|
||||
"description": "주제. 축의 이름을 주제가 정한다.",
|
||||
"evidence": [
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## Elements and evidence
|
||||
|
||||
- **Boundary: 기록은 종류마다 다른 테이블에 산다** (system): No additional description. Evidence: L1071–L1073.
|
||||
- **Boundary: 종류별 테이블** (system): No additional description. Evidence: L1071–L1073.
|
||||
- **topic** (database): 주제. 축의 이름을 주제가 정한다. Evidence: L1057–L1059, L1067–L1068.
|
||||
- **topic_variant** (database): 축의 값들. Evidence: L1060–L1060, L1047–L1048.
|
||||
- **record_variant** (database): 어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. Evidence: L1061–L1061, L1071–L1073.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 주제 안의 축과 기록을 잇는 자리
|
||||
# Question: 하나의 질문에 대한 네 답을 무엇으로 담고, 기록은 어떻게 축에 걸리는가?
|
||||
direction: right
|
||||
g0: "기록은 종류마다 다른 테이블에 산다" {
|
||||
g0: "종류별 테이블" {
|
||||
n3: "document" {
|
||||
shape: sql_table
|
||||
}
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
<?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="topic-variant-model" name="주제 안의 축과 기록을 잇는 자리">
|
||||
<mxGraphModel dx="1444" dy="488" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1444" pageHeight="1169" math="0" shadow="0">
|
||||
<mxGraphModel dx="1385" dy="488" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1385" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="g_record-tables" value="기록은 종류마다 다른 테이블에 산다" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="1189.0" y="35.0" width="210.0" height="408.0" as="geometry"/>
|
||||
<mxCell id="g_record-tables" value="종류별 테이블" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="1130.0" y="35.0" width="210.0" height="408.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_topic" value="topic<br/>variant_label 로 축 이름을 정한다" tooltip="주제. 축의 이름을 주제가 정한다. | Evidence: L1057-L1059, L1067-L1068" 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="70.0" y="213.5" width="209.0" height="71.0" as="geometry"/>
|
||||
<mxCell id="n_topic" value="topic<br/>variant_label" tooltip="주제. 축의 이름을 주제가 정한다. | Evidence: L1057-L1059, L1067-L1068" 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="70.0" y="213.5" width="150.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_topic-variant" value="topic_variant<br/>SPA · Mediator · BFF · Forward-Auth" tooltip="축의 값들. | Evidence: L1060-L1060, L1047-L1048" 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="439.0" y="213.5" width="279.0" height="71.0" as="geometry"/>
|
||||
<mxGeometry x="380.0" y="213.5" width="279.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_record-variant" value="record_variant<br/>(kind, id) 쌍 · 외래키 없음" tooltip="어느 기록이 어느 축에 걸리는지 적는 자리. 외래키를 걸지 못한다. | Evidence: L1061-L1061, L1071-L1073" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
|
||||
<mxGeometry x="878.0" y="213.5" width="181.0" height="71.0" as="geometry"/>
|
||||
<mxGeometry x="819.0" y="213.5" width="181.0" height="71.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_document" value="document" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" 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="1219.0" y="81.0" width="150.0" height="64.0" as="geometry"/>
|
||||
<mxGeometry x="1160.0" y="81.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_open-question" value="open_question" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" 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="1219.0" y="217.0" width="150.0" height="64.0" as="geometry"/>
|
||||
<mxGeometry x="1160.0" y="217.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="n_project-decision" value="project_decision" tooltip="기록 테이블 하나. | Evidence: L1071-L1072" 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="1219.0" y="353.0" width="150.0" height="64.0" as="geometry"/>
|
||||
<mxGeometry x="1160.0" y="353.0" width="150.0" height="64.0" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="e_t1" value="1 : N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_topic" target="n_topic-variant">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="359.0" y="221.0" as="offset"/>
|
||||
<mxPoint x="300.0" y="221.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t2" value="축에 건다" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_topic-variant" target="n_record-variant">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="798.0" y="221.0" as="offset"/>
|
||||
<mxPoint x="739.0" y="221.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t3" value="(kind, id)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_record-variant" target="n_document">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1163.0" y="172.0" as="offset"/>
|
||||
<mxPoint x="1104.0" y="172.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t4" value="(kind, id)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_record-variant" target="n_open-question">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1139.0" y="221.0" as="offset"/>
|
||||
<mxPoint x="1080.0" y="221.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e_t5" value="(kind, id)" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_record-variant" target="n_project-decision">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1163.0" y="326.0" as="offset"/>
|
||||
<mxPoint x="1104.0" y="326.0" as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
</root>
|
||||
|
||||
+29
-29
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"id": "group-record-tables",
|
||||
"type": "rectangle",
|
||||
"x": 1189.0,
|
||||
"x": 1130.0,
|
||||
"y": 35.0,
|
||||
"width": 210.0,
|
||||
"height": 408.0,
|
||||
@@ -36,9 +36,9 @@
|
||||
{
|
||||
"id": "group-label-record-tables",
|
||||
"type": "text",
|
||||
"x": 1205.0,
|
||||
"x": 1146.0,
|
||||
"y": 41.0,
|
||||
"width": 171,
|
||||
"width": 100,
|
||||
"height": 24,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@@ -64,18 +64,18 @@
|
||||
"locked": false,
|
||||
"fontSize": 14,
|
||||
"fontFamily": 5,
|
||||
"text": "기록은 종류마다 다른 테이블에 산다",
|
||||
"text": "종류별 테이블",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "기록은 종류마다 다른 테이블에 산다",
|
||||
"originalText": "종류별 테이블",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "edge-t1",
|
||||
"type": "arrow",
|
||||
"x": 279.0,
|
||||
"x": 220.0,
|
||||
"y": 249.0,
|
||||
"width": 160.0,
|
||||
"height": 0.0,
|
||||
@@ -135,7 +135,7 @@
|
||||
{
|
||||
"id": "edge-label-t1",
|
||||
"type": "text",
|
||||
"x": 314.0,
|
||||
"x": 255.0,
|
||||
"y": 209.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
@@ -174,7 +174,7 @@
|
||||
{
|
||||
"id": "edge-t2",
|
||||
"type": "arrow",
|
||||
"x": 718.0,
|
||||
"x": 659.0,
|
||||
"y": 249.0,
|
||||
"width": 160.0,
|
||||
"height": 0.0,
|
||||
@@ -234,7 +234,7 @@
|
||||
{
|
||||
"id": "edge-label-t2",
|
||||
"type": "text",
|
||||
"x": 753.0,
|
||||
"x": 694.0,
|
||||
"y": 209.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
@@ -273,7 +273,7 @@
|
||||
{
|
||||
"id": "edge-t3",
|
||||
"type": "arrow",
|
||||
"x": 1059.0,
|
||||
"x": 1000.0,
|
||||
"y": 113.0,
|
||||
"width": 160.0,
|
||||
"height": 118.0,
|
||||
@@ -333,7 +333,7 @@
|
||||
{
|
||||
"id": "edge-label-t3",
|
||||
"type": "text",
|
||||
"x": 1118.0,
|
||||
"x": 1059.0,
|
||||
"y": 160.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
@@ -372,7 +372,7 @@
|
||||
{
|
||||
"id": "edge-t4",
|
||||
"type": "arrow",
|
||||
"x": 1059.0,
|
||||
"x": 1000.0,
|
||||
"y": 249.0,
|
||||
"width": 160.0,
|
||||
"height": 0.0,
|
||||
@@ -432,7 +432,7 @@
|
||||
{
|
||||
"id": "edge-label-t4",
|
||||
"type": "text",
|
||||
"x": 1094.0,
|
||||
"x": 1035.0,
|
||||
"y": 209.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
@@ -471,7 +471,7 @@
|
||||
{
|
||||
"id": "edge-t5",
|
||||
"type": "arrow",
|
||||
"x": 1059.0,
|
||||
"x": 1000.0,
|
||||
"y": 267.0,
|
||||
"width": 160.0,
|
||||
"height": 118.0,
|
||||
@@ -531,7 +531,7 @@
|
||||
{
|
||||
"id": "edge-label-t5",
|
||||
"type": "text",
|
||||
"x": 1118.0,
|
||||
"x": 1059.0,
|
||||
"y": 314.0,
|
||||
"width": 90,
|
||||
"height": 24,
|
||||
@@ -572,7 +572,7 @@
|
||||
"type": "rectangle",
|
||||
"x": 70.0,
|
||||
"y": 213.5,
|
||||
"width": 209.0,
|
||||
"width": 150.0,
|
||||
"height": 71.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@@ -602,7 +602,7 @@
|
||||
"type": "text",
|
||||
"x": 80.0,
|
||||
"y": 223.5,
|
||||
"width": 189.0,
|
||||
"width": 130.0,
|
||||
"height": 51.0,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@@ -628,18 +628,18 @@
|
||||
"locked": false,
|
||||
"fontSize": 15,
|
||||
"fontFamily": 5,
|
||||
"text": "topic\nvariant_label 로 축 이름을 정한다",
|
||||
"text": "topic\nvariant_label",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": null,
|
||||
"originalText": "topic\nvariant_label 로 축 이름을 정한다",
|
||||
"originalText": "topic\nvariant_label",
|
||||
"autoResize": true,
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "node-topic-variant",
|
||||
"type": "rectangle",
|
||||
"x": 439.0,
|
||||
"x": 380.0,
|
||||
"y": 213.5,
|
||||
"width": 279.0,
|
||||
"height": 71.0,
|
||||
@@ -669,7 +669,7 @@
|
||||
{
|
||||
"id": "node-label-topic-variant",
|
||||
"type": "text",
|
||||
"x": 449.0,
|
||||
"x": 390.0,
|
||||
"y": 223.5,
|
||||
"width": 259.0,
|
||||
"height": 51.0,
|
||||
@@ -708,7 +708,7 @@
|
||||
{
|
||||
"id": "node-record-variant",
|
||||
"type": "rectangle",
|
||||
"x": 878.0,
|
||||
"x": 819.0,
|
||||
"y": 213.5,
|
||||
"width": 181.0,
|
||||
"height": 71.0,
|
||||
@@ -738,7 +738,7 @@
|
||||
{
|
||||
"id": "node-label-record-variant",
|
||||
"type": "text",
|
||||
"x": 888.0,
|
||||
"x": 829.0,
|
||||
"y": 223.5,
|
||||
"width": 161.0,
|
||||
"height": 51.0,
|
||||
@@ -777,7 +777,7 @@
|
||||
{
|
||||
"id": "node-document",
|
||||
"type": "rectangle",
|
||||
"x": 1219.0,
|
||||
"x": 1160.0,
|
||||
"y": 81.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
@@ -807,7 +807,7 @@
|
||||
{
|
||||
"id": "node-label-document",
|
||||
"type": "text",
|
||||
"x": 1229.0,
|
||||
"x": 1170.0,
|
||||
"y": 91.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
@@ -846,7 +846,7 @@
|
||||
{
|
||||
"id": "node-open-question",
|
||||
"type": "rectangle",
|
||||
"x": 1219.0,
|
||||
"x": 1160.0,
|
||||
"y": 217.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
@@ -876,7 +876,7 @@
|
||||
{
|
||||
"id": "node-label-open-question",
|
||||
"type": "text",
|
||||
"x": 1229.0,
|
||||
"x": 1170.0,
|
||||
"y": 227.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
@@ -915,7 +915,7 @@
|
||||
{
|
||||
"id": "node-project-decision",
|
||||
"type": "rectangle",
|
||||
"x": 1219.0,
|
||||
"x": 1160.0,
|
||||
"y": 353.0,
|
||||
"width": 150.0,
|
||||
"height": 64.0,
|
||||
@@ -945,7 +945,7 @@
|
||||
{
|
||||
"id": "node-label-project-decision",
|
||||
"type": "text",
|
||||
"x": 1229.0,
|
||||
"x": 1170.0,
|
||||
"y": 363.0,
|
||||
"width": 130.0,
|
||||
"height": 44.0,
|
||||
|
||||
+2
-3
@@ -2,7 +2,7 @@
|
||||
"harness_version": "0.2.0",
|
||||
"spec_id": "topic-variant-model",
|
||||
"spec_version": "1.1",
|
||||
"spec_sha256": "c421c2d82e136ffc2312f8203b8b4131cae075cef25dea60169d70126de9894a",
|
||||
"spec_sha256": "9877bf8caf67db577f45c1576cb30b722b4339578a9880267164f759c1c63df4",
|
||||
"source_context": {
|
||||
"document": "document.md",
|
||||
"document_sha256": "93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f",
|
||||
@@ -14,10 +14,9 @@
|
||||
},
|
||||
"outputs": [
|
||||
"topic-variant-model.svg",
|
||||
"topic-variant-model.drawio",
|
||||
"topic-variant-model.mmd",
|
||||
"topic-variant-model.d2",
|
||||
"topic-variant-model.dot",
|
||||
"topic-variant-model.drawio",
|
||||
"topic-variant-model.excalidraw",
|
||||
"topic-variant-model.alt.md"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
%% 주제 안의 축과 기록을 잇는 자리
|
||||
%% question: 하나의 질문에 대한 네 답을 무엇으로 담고, 기록은 어떻게 축에 걸리는가?
|
||||
flowchart LR
|
||||
subgraph g_record_tables["기록은 종류마다 다른 테이블에 산다"]
|
||||
subgraph g_record_tables["종류별 테이블"]
|
||||
n3[("document")]
|
||||
n4[("open_question")]
|
||||
n5[("project_decision")]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1444" height="488" viewBox="0 0 1444 488" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1385" height="488" viewBox="0 0 1385 488" role="img" aria-labelledby="diagram-title diagram-description">
|
||||
<title id="diagram-title">주제 안의 축과 기록을 잇는 자리</title>
|
||||
<desc id="diagram-description">왼쪽에 topic 이 있고 variant_label 로 축의 이름을 스스로 정한다. 그 오른쪽에 topic_variant 가 있고 SPA, Mediator, BFF, Forward-Auth 같은 축의 값들을 담는다. 그 오른쪽에 record_variant 가 있고 어느 기록이 어느 축에 걸리는지를 종류와 아이디의 쌍으로 적는다. record_variant 는 오른쪽의 document, open_question, project_decision 세 테이블을 가리키는데, 기록이 종류마다 다른 테이블에 살기 때문에 외래키를 걸지 못하고 쌍으로만 가리킨다.</desc>
|
||||
<metadata>{"techviz":{"spec_version":"1.1","id":"topic-variant-model","profile":"component-flow"},"source_context":{"document":"document.md","document_sha256":"93b9fec4884efa0e6231de07dc27e2b0ac36c9052d3720e28d102d9747ac4f8f","anchor":{"kind":"marker","value":"topic-variant-model","line":1064}},"evidence_policy":"Each factual element cites source lines or is marked assumption.","diagram_only":true}</metadata>
|
||||
@@ -49,53 +49,53 @@
|
||||
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="canvas" width="1444" height="488" />
|
||||
<rect class="group-box" x="1189.0" y="35.0" width="210.0" height="408.0" rx="8" />
|
||||
<rect class="group-label-bg" x="1203.0" y="25.0" width="155.0" height="22" />
|
||||
<text class="group-label" x="1213.0" y="40.0">기록은 종류마다 다른 테이블에 산다</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="279.0,249.0 359.0,249.0 359.0,249.0 439.0,249.0" data-evidence="1057-1060" />
|
||||
<rect class="edge-label-bg" x="333.2" y="207.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="359.0" y="222.0">1 : N</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="718.0,249.0 798.0,249.0 798.0,249.0 878.0,249.0" data-evidence="1060-1061" />
|
||||
<rect class="edge-label-bg" x="772.2" y="207.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="798.0" y="222.0">축에 건다</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1059.0,231.0 1139.0,231.0 1139.0,113.0 1219.0,113.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1120.5" y="158.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1163.0" y="173.0">(kind, id)</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1059.0,249.0 1139.0,249.0 1139.0,249.0 1219.0,249.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1096.5" y="207.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1139.0" y="222.0">(kind, id)</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1059.0,267.0 1139.0,267.0 1139.0,385.0 1219.0,385.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1120.5" y="312.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1163.0" y="327.0">(kind, id)</text>
|
||||
<rect class="canvas" width="1385" height="488" />
|
||||
<rect class="group-box" x="1130.0" y="35.0" width="210.0" height="408.0" rx="8" />
|
||||
<rect class="group-label-bg" x="1144.0" y="25.0" width="90.0" height="22" />
|
||||
<text class="group-label" x="1154.0" y="40.0">종류별 테이블</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="220.0,249.0 300.0,249.0 300.0,249.0 380.0,249.0" data-evidence="1057-1060" />
|
||||
<rect class="edge-label-bg" x="274.2" y="207.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="300.0" y="222.0">1 : N</text>
|
||||
<polyline class="edge kind-data style-solid emphasis-normal" points="659.0,249.0 739.0,249.0 739.0,249.0 819.0,249.0" data-evidence="1060-1061" />
|
||||
<rect class="edge-label-bg" x="713.2" y="207.0" width="51.5" height="22" rx="3" />
|
||||
<text class="edge-label" x="739.0" y="222.0">축에 건다</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,231.0 1080.0,231.0 1080.0,113.0 1160.0,113.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1061.5" y="158.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1104.0" y="173.0">(kind, id)</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,249.0 1080.0,249.0 1080.0,249.0 1160.0,249.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1037.5" y="207.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1080.0" y="222.0">(kind, id)</text>
|
||||
<polyline class="edge kind-data style-dashed emphasis-normal" points="1000.0,267.0 1080.0,267.0 1080.0,385.0 1160.0,385.0" data-evidence="1061-1073" />
|
||||
<rect class="edge-label-bg" x="1061.5" y="312.0" width="85.0" height="22" rx="3" />
|
||||
<text class="edge-label" x="1104.0" y="327.0">(kind, id)</text>
|
||||
<g id="node-topic">
|
||||
<rect class="node-shape kind-database emphasis-normal role-source" data-evidence="1057-1059,1067-1068" x="70.0" y="213.5" width="209.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="174.5" y="240.5">topic</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="261.5" x2="265.0" y2="261.5" />
|
||||
<text class="node-detail" x="86.0" y="278.5">variant_label 로 축 이름을 정한다</text>
|
||||
<rect class="node-shape kind-database emphasis-normal role-source" data-evidence="1057-1059,1067-1068" x="70.0" y="213.5" width="150.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="145.0" y="240.5">topic</text>
|
||||
<line class="node-detail-divider" x1="84.0" y1="261.5" x2="206.0" y2="261.5" />
|
||||
<text class="node-detail" x="86.0" y="278.5">variant_label</text>
|
||||
</g>
|
||||
<g id="node-topic-variant">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1060-1060,1047-1048" x="439.0" y="213.5" width="279.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="578.5" y="240.5">topic_variant</text>
|
||||
<line class="node-detail-divider" x1="453.0" y1="261.5" x2="704.0" y2="261.5" />
|
||||
<text class="node-detail" x="455.0" y="278.5">SPA · Mediator · BFF · Forward-Auth</text>
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1060-1060,1047-1048" x="380.0" y="213.5" width="279.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="519.5" y="240.5">topic_variant</text>
|
||||
<line class="node-detail-divider" x1="394.0" y1="261.5" x2="645.0" y2="261.5" />
|
||||
<text class="node-detail" x="396.0" y="278.5">SPA · Mediator · BFF · Forward-Auth</text>
|
||||
</g>
|
||||
<g id="node-record-variant">
|
||||
<rect class="node-shape kind-database emphasis-primary role-store" data-evidence="1061-1061,1071-1073" x="878.0" y="213.5" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="968.5" y="240.5">record_variant</text>
|
||||
<line class="node-detail-divider" x1="892.0" y1="261.5" x2="1045.0" y2="261.5" />
|
||||
<text class="node-detail" x="894.0" y="278.5">(kind, id) 쌍 · 외래키 없음</text>
|
||||
<rect class="node-shape kind-database emphasis-primary role-store" data-evidence="1061-1061,1071-1073" x="819.0" y="213.5" width="181.0" height="71.0" rx="7" />
|
||||
<text class="node-label" x="909.5" y="240.5">record_variant</text>
|
||||
<line class="node-detail-divider" x1="833.0" y1="261.5" x2="986.0" y2="261.5" />
|
||||
<text class="node-detail" x="835.0" y="278.5">(kind, id) 쌍 · 외래키 없음</text>
|
||||
</g>
|
||||
<g id="node-document">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1219.0" y="81.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1294.0" y="111.0">document</text>
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="81.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1235.0" y="111.0">document</text>
|
||||
</g>
|
||||
<g id="node-open-question">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1219.0" y="217.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1294.0" y="247.0">open_question</text>
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="217.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1235.0" y="247.0">open_question</text>
|
||||
</g>
|
||||
<g id="node-project-decision">
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1219.0" y="353.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1294.0" y="383.0">project_decision</text>
|
||||
<rect class="node-shape kind-database emphasis-normal role-store" data-evidence="1071-1072" x="1160.0" y="353.0" width="150.0" height="64.0" rx="7" />
|
||||
<text class="node-label" x="1235.0" y="383.0">project_decision</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
+1
-1
@@ -30,7 +30,7 @@ source:
|
||||
|
||||
주제 화면의 네 줄(SPA·Mediator·BFF·Forward-Auth)은 링크로 그려져 있었다. 눌러도 아무 일이 없었다.
|
||||
|
||||
처음에 `/topics/{주제}/{축}` 이라 적어 두었는데 그런 화면이 없었다.
|
||||
처음에 /topics/{주제}/{축} 이라 적어 두었는데 그런 화면이 없었다.
|
||||
|
||||
## 결론
|
||||
|
||||
|
||||
+6
-6
@@ -35,14 +35,14 @@ source:
|
||||
|
||||
간헐적으로 보인 것은 두 가지 결정적 어긋남이었고, 사용자는 둘 다 만났다.
|
||||
|
||||
`인증` : 남는 글자가 없어 빈 문자열 → 폼이 요청 전에 거절
|
||||
`Redis 캐시` 와 `Redis 클러스터` : 둘 다 `redis` → 두 번째가 충돌
|
||||
인증 : 남는 글자가 없어 빈 문자열 → 폼이 요청 전에 거절
|
||||
Redis 캐시 와 Redis 클러스터 : 둘 다 redis → 두 번째가 충돌
|
||||
|
||||
> 규칙은 간헐적이었던 적이 없다. **보이지 않았을 뿐이다** — slug 생성이 `[a-z0-9]` 만 남기고 나머지를 버려서, 한글 이름은 아무것도 기여하지 못했다.
|
||||
> 규칙은 간헐적이었던 적이 없다. **보이지 않았을 뿐이다** — slug 생성이 [a-z0-9] 만 남기고 나머지를 버려서, 한글 이름은 아무것도 기여하지 못했다.
|
||||
|
||||
한글을 버리지 않고 로마자로 옮긴다. 음절을 초성·중성·종성으로 산술 분해하므로 표가 필요 없고 결정적이다.
|
||||
|
||||
`백엔드 아키텍처` → `baekendeu-akitekcheo`
|
||||
백엔드 아키텍처 → baekendeu-akitekcheo
|
||||
|
||||
국어의 로마자 표기법의 자모 대응만 적용하고 음운 변화 규칙은 일부러 뺐다. slug 는 읽는 것이지 발음하는 것이 아니고, 그 규칙을 넣으면 같은 이름이 문맥에 따라 다른 slug 가 된다.
|
||||
|
||||
@@ -54,8 +54,8 @@ tech-log-frontend : 5cffe30 · 7093d84
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 주제 이름을 `인증` 으로 적고 저장한다 — 옛 규칙에서는 빈 slug 가 되어 폼이 거절한다
|
||||
2. `Redis 캐시` 와 `Redis 클러스터` 를 차례로 만든다 — 옛 규칙에서는 두 번째가 충돌한다
|
||||
1. 주제 이름을 인증 으로 적고 저장한다 — 옛 규칙에서는 빈 slug 가 되어 폼이 거절한다
|
||||
2. Redis 캐시 와 Redis 클러스터 를 차례로 만든다 — 옛 규칙에서는 두 번째가 충돌한다
|
||||
3. 새 규칙에서 같은 이름들의 slug 를 확인한다
|
||||
|
||||
## 본문
|
||||
|
||||
+2
-6
@@ -35,7 +35,7 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
`/references/external-idp-federation-application-boundary` 의 「다음에 읽을 것」 두 번째 항목이 404 였다.
|
||||
/references/external-idp-federation-application-boundary 의 「다음에 읽을 것」 두 번째 항목이 404 였다.
|
||||
|
||||
화면 코드 어디에도 그 주소를 만드는 곳이 없다. 주소는 게시할 때 서버가 만들어 DB 에 저장한 문자열이고, 화면은 그것을 그대로 링크로 그린다.
|
||||
|
||||
@@ -76,8 +76,7 @@ tech-log-frontend : fe6b56a
|
||||
|
||||
## 주소가 만들어져 저장되고 방문에서 끝난다
|
||||
|
||||
:::evidence key="decision-path-404" alt="계약·게시 시점 경로 생성·저장 테이블·조회 시점 경로 생성·방문자·공개 라우트 여섯 참가자 사이의 순서도" caption=" " zoom="true"
|
||||
:::
|
||||

|
||||
|
||||
계약은 결정의 공개 주소가 목록 위의 앵커라고 규정한다. 게시 시점의 경로 생성기는 그 대신 목록 아래에 slug 를 붙인 경로를 만들어 공개 투영에 저장한다. 조회 시점의 다른 생성기가 저장된 주소를 읽어 방문자에게 링크로 내보낸다. 방문자가 그 주소를 요청하면 공개 라우트에는 목록 하나뿐이라 맞는 라우트가 없다.
|
||||
|
||||
@@ -112,9 +111,6 @@ tech-log-frontend : fe6b56a
|
||||
|
||||
배포 후 사이트 전체를 훑어 서버가 내보내는 주소 26개와 주제·축 9개를 더해 35개 전부 200 인 것을 확인했다.
|
||||
|
||||
:::evidence key="dead-link-sweep" alt="서버가 내보내는 주소 35개를 전수로 훑은 감사 출력" caption=" " zoom="false"
|
||||
:::
|
||||
|
||||
같은 방식으로 다시 검사하는 스크립트를 증거와 함께 남겼다. 다음에 라우트를 더하면 그 스크립트를 다시 돌린다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
+8
-21
@@ -7,13 +7,6 @@ topicName: 주제 안의 축
|
||||
project: TechLog
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
assets:
|
||||
- key: home-tabs-keycloak
|
||||
file: ../../../final/evidence/browser/home-tabs-keycloak.png
|
||||
- key: home-topic-tabs-2
|
||||
file: ../../../final/evidence/browser/home-topic-tabs-2.png
|
||||
- key: home-tabs-grouped
|
||||
file: ../../../final/evidence/browser/home-tabs-grouped.png
|
||||
evidence:
|
||||
- ../../../final/evidence/browser/home-tabs-keycloak.png
|
||||
- ../../../final/evidence/browser/home-topic-tabs.png
|
||||
@@ -46,11 +39,14 @@ source:
|
||||
|
||||
세 단계를 거쳤다.
|
||||
|
||||
| 단계 | 무엇 | 왜 바꿨나 |
|
||||
|---|---|---|
|
||||
| 1 | 주제 하나만 펼치고 아래 「다른 주제 N개 보기」 한 줄 | 홈이 「무엇을 만들었나」로 시작했다 |
|
||||
| 2 | 제목 자리를 주제 이름 탭이 대신 (30px/650) | 「다른 주제」 줄은 목록을 다 읽고 나서야 만나는 곳이라 대개 지나쳤다 |
|
||||
| 3 | 탭을 칩 크기로 낮추고 개수 상한 제거 | 주제가 열 개, 스무 개가 되면 이름만으로 화면이 덮인다 |
|
||||
1단계 : 주제 하나만 펼치고 아래에 「다른 주제 N개 보기」 한 줄
|
||||
바꾼 이유 : 홈이 「무엇을 만들었나」로 시작했다
|
||||
|
||||
2단계 : 제목 자리를 주제 이름 탭이 대신한다 (30px/650)
|
||||
바꾼 이유 : 「다른 주제」 줄은 목록을 다 읽고 나서야 만나는 곳이라 대개 지나쳤다
|
||||
|
||||
3단계 : 탭을 칩 크기로 낮추고 개수 상한을 없앴다
|
||||
바꾼 이유 : 주제가 열 개, 스무 개가 되면 이름만으로 화면이 덮인다
|
||||
|
||||
3단계에서 요청 구조를 바꿨다. 탭은 목록 호출 하나가 주는 전부이고, 상세는 고른 탭만 그때 받아 캐시한다. 그래서 주제가 몇 개가 되든 홈이 처음 보내는 요청은 목록 하나와 주제 하나로 고정된다.
|
||||
|
||||
@@ -79,9 +75,6 @@ tech-log-frontend : 604ded5 → de4cb8b → 3bb724b · 2b2f443
|
||||
|
||||
3단계에서 탭을 칩 크기로 낮추고 개수 상한을 없앴다. 주제가 열 개, 스무 개가 되면 이름만으로 화면이 덮이기 때문이다.
|
||||
|
||||
:::evidence key="home-tabs-keycloak" alt="탭을 구역 제목 급으로 세운 2단계 화면" caption=" " zoom="true"
|
||||
:::
|
||||
|
||||
## 상한이 왜 있었나
|
||||
|
||||
상한은 주제마다 상세를 미리 받느라 둔 것이었다. 상세를 다 받으려면 요청이 주제 수만큼 늘어나므로 그 수를 제한해야 했다.
|
||||
@@ -100,14 +93,8 @@ tech-log-frontend : 604ded5 → de4cb8b → 3bb724b · 2b2f443
|
||||
|
||||
칩으로 낮추니 목록 위에 글자만 떠 있는 것처럼 보였다. 고른 것이 색으로만 달라 누를 수 있는 것으로 읽히지 않았고, 탭 줄과 목록 사이가 선 없이 30px 비어 두 덩어리로 갈렸다.
|
||||
|
||||
:::evidence key="home-topic-tabs-2" alt="칩으로 낮춘 직후의 비교 구역 확대" caption=" " zoom="true"
|
||||
:::
|
||||
|
||||
고른 탭에 알약 형태를 주고, 묶음의 윗선을 목록이 아니라 패널이 갖게 해서 탭 줄이 그 선에 바로 얹히게 했다.
|
||||
|
||||
:::evidence key="home-tabs-grouped" alt="고른 탭에 형태를 주고 탭 줄을 패널 윗선에 얹은 최종 화면" caption=" " zoom="true"
|
||||
:::
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
주제가 스무 개일 때의 화면은 만들어 보지 않았다. 상한을 없앤 근거는 요청 구조이지 그 규모의 측정이 아니다.
|
||||
|
||||
+1
-2
@@ -45,8 +45,7 @@ topic (주제)
|
||||
└─ record_variant 어느 기록이 어느 축에 걸리는지 (kind, id) 쌍
|
||||
```
|
||||
|
||||
:::evidence key="topic-variant-model" alt="topic·topic_variant·record_variant 가 이어지고 record_variant 가 세 테이블을 가리키는 구조도" caption=" " zoom="true"
|
||||
:::
|
||||

|
||||
|
||||
## 축 이름은 주제가 정한다
|
||||
|
||||
|
||||
+2
-2
@@ -37,13 +37,13 @@ source:
|
||||
|
||||
사용자가 원인을 정확히 짚어 주었다 — 「디자인이 안 된 게 아니라 CSS 선택자가 새고 있습니다」.
|
||||
|
||||
```css
|
||||
css
|
||||
.home-comparison a {
|
||||
display: grid;
|
||||
grid-template-columns: 200px minmax(0, 1fr);
|
||||
padding: 27px 2px 28px;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
비교 행을 위한 규칙인데 선택자가 구역 전체라 제목 안의 링크까지 잡았다. 제목이 200px 칸에 갇혀 두 줄로 접히고 행용 padding 까지 물려 h2 높이가 199px 이 됐다.
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ source:
|
||||
|
||||
### 1. 배치 속성은 그 배치를 쓰는 요소까지 좁혀 적는다
|
||||
|
||||
`display: grid|flex`·`grid-template-columns`·`padding` 을 구역 클래스 아래 태그 선택자로 걸지 않는다.
|
||||
display: grid|flex·grid-template-columns·padding 을 구역 클래스 아래 태그 선택자로 걸지 않는다.
|
||||
|
||||
행을 위한 격자를 구역 전체에 걸면 제목 안의 링크도 그 격자가 된다. 첫 칸 너비에 갇혀 접히고 행용 padding 까지 물린다.
|
||||
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
관리 계약의 연산은 `tech-log-management-contract-contribution.ts` 에 등록해야 실행 시 부를 수 있다. 계약에서 타입은 생성되므로 등록을 빠뜨려도 컴파일은 통과한다.
|
||||
관리 계약의 연산은 tech-log-management-contract-contribution.ts 에 등록해야 실행 시 부를 수 있다. 계약에서 타입은 생성되므로 등록을 빠뜨려도 컴파일은 통과한다.
|
||||
|
||||
등록되지 않은 연산을 부르면 게이트웨이가 그 연산을 찾지 못하고 옆의 분기로 떨어진다. 그래서 증상이 「없는 연산」이 아니라 「다른 연산이 실행됨」으로 나온다.
|
||||
|
||||
@@ -34,9 +34,9 @@ source:
|
||||
|
||||
네 번 났고 전부 같은 원인이었다.
|
||||
|
||||
`getPublicConcept` : 개념 화면이 질문 조회를 불렀다
|
||||
`deleteConceptDraft` : 개념 삭제가 질문 삭제를 불렀다
|
||||
`listStudioQuestions` · `listStudioProjectDecisions` : 홈 편집기가 빈 목록을 그렸다
|
||||
getPublicConcept : 개념 화면이 질문 조회를 불렀다
|
||||
deleteConceptDraft : 개념 삭제가 질문 삭제를 불렀다
|
||||
listStudioQuestions · listStudioProjectDecisions : 홈 편집기가 빈 목록을 그렸다
|
||||
축(variant) CRUD 네 연산 : 축 화면이 데이터를 받지 못했다
|
||||
|
||||
공개 계약은 전수 대조하고, 관리 계약은 「한 종류만 빠진 항목」을 보는 가드를 뒀다. 깨진 것이 늘 그 모양이었다.
|
||||
|
||||
+2
-2
@@ -48,13 +48,13 @@ source:
|
||||
|
||||
tech-log-backend : 365560e 이후
|
||||
tech-log-frontend : 계약에서 생성한 타입을 그대로 사용
|
||||
확인 방식 : 계약이 선언한 연산과 `@RestController` 매핑을 리플렉션으로 대조
|
||||
확인 방식 : 계약이 선언한 연산과 @RestController 매핑을 리플렉션으로 대조
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 계약에 연산을 하나 선언하고 컨트롤러는 만들지 않는다
|
||||
2. 프론트에서 그 연산을 부르는 화면을 연다 — 404 가 돌아오고 화면은 빈 목록을 그린다
|
||||
3. `ContractRouteCoverageTest` 를 돌린다 — 그 연산 하나를 짚는다
|
||||
3. ContractRouteCoverageTest 를 돌린다 — 그 연산 하나를 짚는다
|
||||
|
||||
## 본문
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ source:
|
||||
|
||||
### 1. 서버 쪽은 매핑을 리플렉션으로 모아 계약의 경로와 전수 대조한다
|
||||
|
||||
`@RestController` 들을 훑어 실제 매핑을 모으고 계약이 선언한 경로 전부와 맞춘다. 기대 목록을 손으로 적으면 그 목록이 또 하나의 손 목록이 되므로 계약에서 읽는다.
|
||||
@RestController 들을 훑어 실제 매핑을 모으고 계약이 선언한 경로 전부와 맞춘다. 기대 목록을 손으로 적으면 그 목록이 또 하나의 손 목록이 되므로 계약에서 읽는다.
|
||||
|
||||
### 2. 화면 쪽은 계약이 선언한 연산이 기여 목록에 등록됐는지 본다
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ source:
|
||||
|
||||
더 미묘한 변종이 하나 더 있었다.
|
||||
|
||||
> `Promise.all([gateway.foo()])` 은 foo 가 **거절하는 것만** 잡는다. 호출이 **동기적으로 던지면** 배열을 만드는 중에 터져 rejection handler 를 지나지 못하고, 그러면 홈 focus 한 칸 때문에 대시보드 전체가 빈 화면이 된다.
|
||||
> Promise.all([gateway.foo()]) 은 foo 가 **거절하는 것만** 잡는다. 호출이 **동기적으로 던지면** 배열을 만드는 중에 터져 rejection handler 를 지나지 못하고, 그러면 홈 focus 한 칸 때문에 대시보드 전체가 빈 화면이 된다.
|
||||
|
||||
같은 판단을 주제 탭에도 적용했다.
|
||||
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ source:
|
||||
|
||||
# 매퍼가 null 을 돌려주고 호출부가 걸러 내, 기록이 조용히 사라졌다
|
||||
|
||||
프로젝트 기록 목록에서 Open Question 이 보이지 않았다. 이 목록은 탐색의 지식 목록과 응답 모양이 다른데 그쪽 매퍼를 쓰고 있었다. 그 매퍼는 두 종류가 아니면 `null` 을 돌려주고 호출부가 걸러 내므로, 질문과 개념은 오류도 빈 줄도 남기지 않고 사라진다.
|
||||
프로젝트 기록 목록에서 Open Question 이 보이지 않았다. 이 목록은 탐색의 지식 목록과 응답 모양이 다른데 그쪽 매퍼를 쓰고 있었다. 그 매퍼는 두 종류가 아니면 null 을 돌려주고 호출부가 걸러 내므로, 질문과 개념은 오류도 빈 줄도 남기지 않고 사라진다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -35,7 +35,7 @@ source:
|
||||
|
||||
프로젝트 기록 목록은 탐색의 지식 목록과 응답 모양이 다른데 지식 목록의 매퍼를 그대로 쓰고 있었다.
|
||||
|
||||
그 매퍼는 CASE 나 REFERENCE 가 아니면 `null` 을 돌려준다. 호출부가 `filter` 로 걸러 내므로 그 항목은 목록에서 없어진다.
|
||||
그 매퍼는 CASE 나 REFERENCE 가 아니면 null 을 돌려준다. 호출부가 filter 로 걸러 내므로 그 항목은 목록에서 없어진다.
|
||||
|
||||
증상 : 목록이 한 줄 짧아진다
|
||||
오류 : 없음
|
||||
|
||||
+6
-9
@@ -19,7 +19,7 @@ source:
|
||||
|
||||
# 개념을 하나 더하자 열세 곳이 그것을 조용히 삼켰다
|
||||
|
||||
문서 종류에 CONCEPT 을 더했다. 컴파일도 통과하고 테스트도 통과했는데, 개념을 지우면 「질문을 찾을 수 없습니다」가 나오고 개념 상세 주소는 404 였고 탐색에서 `type=CONCEPT` 은 0건이었다. 그 뒤로도 같은 모양이 계속 나와 열세 번을 셌다. 매번 원인이 같았다 — 다섯 종류를 손으로 적어 둔 곳이 있었고 새 종류가 마지막 `else` 로 떨어졌다.
|
||||
문서 종류에 CONCEPT 을 더했다. 컴파일도 통과하고 테스트도 통과했는데, 개념을 지우면 「질문을 찾을 수 없습니다」가 나오고 개념 상세 주소는 404 였고 탐색에서 type=CONCEPT 은 0건이었다. 그 뒤로도 같은 모양이 계속 나와 열세 번을 셌다. 매번 원인이 같았다 — 다섯 종류를 손으로 적어 둔 곳이 있었고 새 종류가 마지막 else 로 떨어졌다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -36,7 +36,7 @@ source:
|
||||
|
||||
문서 종류는 다섯이다 — CASE, REFERENCE, QUESTION, CONCEPT, PROJECT_DECISION. 새 종류를 하나 더하면 그 종류를 아는 곳이 전부 함께 늘어나야 한다.
|
||||
|
||||
실제로는 늘지 않은 곳이 열세 곳 있었고, 그중 어느 곳도 오류를 내지 않았다. 삼항 사슬의 마지막 `else` 와 배열 리터럴의 끝이 모르는 값을 조용히 받아 갔다.
|
||||
실제로는 늘지 않은 곳이 열세 곳 있었고, 그중 어느 곳도 오류를 내지 않았다. 삼항 사슬의 마지막 else 와 배열 리터럴의 끝이 모르는 값을 조용히 받아 갔다.
|
||||
|
||||
## 결론
|
||||
|
||||
@@ -57,10 +57,10 @@ tech-log-backend : 8cd8ee3
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. `PublicRecord["kind"]` 에 새 값을 하나 더한다
|
||||
2. `npm run check:types` 를 돌린다 — `Record<Kind, _>` 로 바꾼 곳은 여기서 멈춘다
|
||||
3. 계약의 enum 에서 CONCEPT 을 빼고 `StudioContractUnionJacksonTest` 를 돌린다 — 빨개진다
|
||||
4. `PublicSql.pathOf` 에 새 값을 넣지 않고 그 종류를 게시한다 — 컴파일은 통과하고 경로가 null 로 나간다
|
||||
1. PublicRecord["kind"] 에 새 값을 하나 더한다
|
||||
2. npm run check:types 를 돌린다 — Record<Kind, _> 로 바꾼 곳은 여기서 멈춘다
|
||||
3. 계약의 enum 에서 CONCEPT 을 빼고 StudioContractUnionJacksonTest 를 돌린다 — 빨개진다
|
||||
4. PublicSql.pathOf 에 새 값을 넣지 않고 그 종류를 게시한다 — 컴파일은 통과하고 경로가 null 로 나간다
|
||||
|
||||
## 본문
|
||||
|
||||
@@ -154,7 +154,4 @@ export const EXPLORE_KIND_PATHS: Record<RecordKind, string> = {
|
||||
|
||||
두 곳이 아직 표가 아니다. 공개 경로 생성기는 분기 대상이 문자열이라 컴파일러가 셀 수 있는 값 집합이 없고, 작업본 검증기의 문자열 칸 목록은 사슬로 남아 있다.
|
||||
|
||||
:::evidence key="kind-tables-now" alt="현재 코드에서 종류를 나열하는 곳을 조회한 출력" caption=" " zoom="false"
|
||||
:::
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# 컴파일러가 빠진 가지를 요구하게 만드는 두 가지 — Record 표와 sealed switch 식
|
||||
|
||||
같은 언어 안에서도 어떤 분기는 새 값을 더할 때 컴파일러가 빠진 값을 짚고 어떤 분기는 아무 말도 하지 않는다. 삼항 사슬과 배열 리터럴은 후자이고, `Record<Kind, _>` 와 식으로 쓴 sealed switch 는 전자다.
|
||||
같은 언어 안에서도 어떤 분기는 새 값을 더할 때 컴파일러가 빠진 값을 짚고 어떤 분기는 아무 말도 하지 않는다. 삼항 사슬과 배열 리터럴은 후자이고, Record<Kind, _> 와 식으로 쓴 sealed switch 는 전자다.
|
||||
|
||||
## 관계
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ source:
|
||||
|
||||
## 사실
|
||||
|
||||
공개 경로 생성기는 문서 종류가 아니라 공개 투영의 종류 칸(String)으로 switch 하고 `default -> null` 이 남아 있다.
|
||||
공개 경로 생성기는 문서 종류가 아니라 공개 투영의 종류 칸(String)으로 switch 하고 default -> null 이 남아 있다.
|
||||
|
||||
그 칸은 문서 종류 다섯에 더해 PROJECT 와 RELEASE 도 담는다. 지금 그 switch 가 다루는 값은 여덟이다.
|
||||
|
||||
|
||||
+2
-2
@@ -31,13 +31,13 @@ source:
|
||||
|
||||
새 값을 더했을 때 오류 없이 잘못된 값이 나가는 것을 막는다.
|
||||
|
||||
이 부류는 컴파일도 테스트도 지나간다. 마지막 `else` 가 모르는 값을 받아 가면 모든 값에 갈 곳이 있고 각 가지가 내놓는 타입도 같으므로, 타입 검사가 물을 것이 남지 않는다. 그래서 배포된 뒤 사용자가 만나기 전까지 아무도 모른다.
|
||||
이 부류는 컴파일도 테스트도 지나간다. 마지막 else 가 모르는 값을 받아 가면 모든 값에 갈 곳이 있고 각 가지가 내놓는 타입도 같으므로, 타입 검사가 물을 것이 남지 않는다. 그래서 배포된 뒤 사용자가 만나기 전까지 아무도 모른다.
|
||||
|
||||
## 규칙
|
||||
|
||||
### 1. 유한한 집합의 분기는 값마다 항목을 요구하는 형태로 쓴다
|
||||
|
||||
`Record<K, V>` 는 키 집합이 `K` 와 정확히 같은 객체 타입이라, 키가 하나 모자라면 그 리터럴이 그 타입이 아니게 된다. `K` 에 값을 더하면 리터럴을 쓴 곳이 전부 타입 오류가 된다. Java 에서는 sealed 타입을 대상으로 switch 를 식으로 쓴다 — 식은 값을 내놓아야 하므로 모든 경우에 무엇을 반환할지 컴파일러가 요구한다. 문으로 쓴 switch 는 요구하지 않는다.
|
||||
Record<K, V> 는 키 집합이 K 와 정확히 같은 객체 타입이라, 키가 하나 모자라면 그 리터럴이 그 타입이 아니게 된다. K 에 값을 더하면 리터럴을 쓴 곳이 전부 타입 오류가 된다. Java 에서는 sealed 타입을 대상으로 switch 를 식으로 쓴다 — 식은 값을 내놓아야 하므로 모든 경우에 무엇을 반환할지 컴파일러가 요구한다. 문으로 쓴 switch 는 요구하지 않는다.
|
||||
|
||||
### 2. 키가 그 열거형이 아니면 표로 좁히지 말고 그 이유를 코드 옆에 적는다
|
||||
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# nginx 가 모르는 라우트는 새로고침에서 404 다
|
||||
|
||||
`/studio/releases` 가 평문 404 를 돌려줬다. 라우트는 있고 청크도 빌드됐고 SPA 내부 이동으로는 화면에 닿는데, 하드 로드와 새로고침은 거기까지 가지 못한다. nginx 설정이 손으로 유지하는 배열에서 나오고 있었다.
|
||||
/studio/releases 가 평문 404 를 돌려줬다. 라우트는 있고 청크도 빌드됐고 SPA 내부 이동으로는 화면에 닿는데, 하드 로드와 새로고침은 거기까지 가지 못한다. nginx 설정이 손으로 유지하는 배열에서 나오고 있었다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -36,11 +36,11 @@ SPA 안에서 이동하면 화면이 열린다. 주소창에 그 주소를 직
|
||||
|
||||
서빙 계약의 절반은 라우트 레지스트리에서 유도하고 있었고 나머지 절반은 손으로 유지하는 배열이었다.
|
||||
|
||||
> 서빙 계약의 공개 절반은 라우트 레지스트리에서 패턴을 유도한다. **Studio 절반은 손으로 유지하는 배열이었고, 손으로 유지하는 배열이 실패하는 방식 그대로 실패했다** — `^/studio/assets$` 위의 주석이 바로 그 버그를 한 번 고친 기록이고, 라우트를 더하니 즉시 반복됐다.
|
||||
> 서빙 계약의 공개 절반은 라우트 레지스트리에서 패턴을 유도한다. **Studio 절반은 손으로 유지하는 배열이었고, 손으로 유지하는 배열이 실패하는 방식 그대로 실패했다** — ^/studio/assets$ 위의 주석이 바로 그 버그를 한 번 고친 기록이고, 라우트를 더하니 즉시 반복됐다.
|
||||
|
||||
공개 절반도 유도라고 하기 어려웠다. 번들된 픽스처에 우연히 들어 있던 공개 경로를 전부 열거하고, 생성된 nginx 가 정확히 그것들을 `location =` 블록으로 게시했다. 빌드 이후에 게시된 기록은 SPA 에 묻기도 전에 엣지에서 404 였다. 경로 27개가 얼어 있었고 28번째는 무엇이든 닿을 수 없었다.
|
||||
공개 절반도 유도라고 하기 어려웠다. 번들된 픽스처에 우연히 들어 있던 공개 경로를 전부 열거하고, 생성된 nginx 가 정확히 그것들을 location = 블록으로 게시했다. 빌드 이후에 게시된 기록은 SPA 에 묻기도 전에 엣지에서 404 였다. 경로 27개가 얼어 있었고 28번째는 무엇이든 닿을 수 없었다.
|
||||
|
||||
지금은 라우트 계약에서 등록된 Public 라우트마다 정규식 하나를 만든다. 파라미터는 한 세그먼트만 잡고 슬래시는 잡지 않으므로 `/cases/a/b` 는 404 로 남는다.
|
||||
지금은 라우트 계약에서 등록된 Public 라우트마다 정규식 하나를 만든다. 파라미터는 한 세그먼트만 잡고 슬래시는 잡지 않으므로 /cases/a/b 는 404 로 남는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ source:
|
||||
|
||||
그러면 깨진 링크를 상태 코드로 판정하는 쪽이 전부 못 본다. 크롤러도 못 보고, 서버가 내보내는 주소를 전수로 훑는 감사도 못 본다. 그 감사는 이 저장소에서 실제로 결함을 잡은 방법이고, soft 200 이 섞이면 감사가 통과하면서 방문자만 빈 화면을 만난다.
|
||||
|
||||
파라미터가 슬래시를 잡지 않게 한 것도 같은 이유다. `/cases/a/b` 가 404 로 남아야 그 주소가 잘못됐다는 것이 드러난다. 슬래시까지 잡으면 세그먼트가 몇 개든 라우트에 걸리고, 라우터가 그것을 「없는 기록」으로 그린다.
|
||||
파라미터가 슬래시를 잡지 않게 한 것도 같은 이유다. /cases/a/b 가 404 로 남아야 그 주소가 잘못됐다는 것이 드러난다. 슬래시까지 잡으면 세그먼트가 몇 개든 라우트에 걸리고, 라우터가 그것을 「없는 기록」으로 그린다.
|
||||
|
||||
대안은 catch-all 을 번역하고 라우터가 404 화면을 그리게 하는 것이었다. 사람에게 보이는 화면은 같지만 기계가 읽는 상태 코드가 달라지므로 고르지 않았다.
|
||||
|
||||
|
||||
+2
-2
@@ -32,9 +32,9 @@ FE-GATE-009 는 설치된 라우트마다 증거 파일 하나를 요구하고,
|
||||
|
||||
정확한 일치를 요구하는 이유는 빠뜨림이 통과가 되지 않게 하려는 것이다. 파일이 더 많아도 더 적어도 거절한다.
|
||||
|
||||
`artifacts/tests/a11y-manual/*.md` 는 전부 `pending-manual-review` 다.
|
||||
artifacts/tests/a11y-manual/*.md 는 전부 pending-manual-review 다.
|
||||
|
||||
`review:a11y-manual` 스크립트는 그래서 실패하는 것이 지금은 정상이다.
|
||||
review:a11y-manual 스크립트는 그래서 실패하는 것이 지금은 정상이다.
|
||||
|
||||
라우트를 더할 때마다 이 증거 개수가 함께 움직였고, 커밋 넷에서 111 → 117 로 늘었다.
|
||||
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ source:
|
||||
|
||||
### 2. 빌드가 아는 목록을 서빙 계약의 근거로 쓰지 않는다
|
||||
|
||||
번들된 픽스처에 우연히 들어 있던 경로를 열거하면 그 목록이 빌드 시점에 얼어붙는다. `location =` 은 정확히 일치하는 경로만 잡으므로, 빌드 이후에 게시된 기록은 SPA 에 묻기도 전에 엣지에서 404 가 된다.
|
||||
번들된 픽스처에 우연히 들어 있던 경로를 열거하면 그 목록이 빌드 시점에 얼어붙는다. location = 은 정확히 일치하는 경로만 잡으므로, 빌드 이후에 게시된 기록은 SPA 에 묻기도 전에 엣지에서 404 가 된다.
|
||||
|
||||
### 3. 유도할 수 없는 목록에는 대조 검사를 둔다
|
||||
|
||||
@@ -68,7 +68,7 @@ source:
|
||||
|
||||
## 예시
|
||||
|
||||
`/studio/releases` 가 평문 404 였다. 라우트도 청크도 있었고 서빙 계약의 손 배열에만 없었다.
|
||||
/studio/releases 가 평문 404 였다. 라우트도 청크도 있었고 서빙 계약의 손 배열에만 없었다.
|
||||
|
||||
- 같은 배열을 한 번 고치면서 왜 고쳤는지를 주석으로 남겨 두었는데, 다음 사람이 그 배열에 줄을 더할 때 그 주석을 읽지 않았다.
|
||||
|
||||
|
||||
+2
-2
@@ -30,10 +30,10 @@ source:
|
||||
|
||||
홈 화면 하나에 종류 이름이 아홉 개 있었다.
|
||||
|
||||
```text
|
||||
text
|
||||
최근 기록 목록: CASE · CONCEPT · OPEN QUESTION · REFERENCE
|
||||
바로 아래 「종류별로 읽기」: 검증 기록 · 동작 원리 · 적용 기준 · 열린 질문
|
||||
```
|
||||
|
||||
|
||||
두 목록이 같은 다섯 종류를 가리키는데 이름이 겹치지 않는다. 독자는 그 둘이 같은 것이라는 단서를 받지 못한다.
|
||||
|
||||
|
||||
+4
-4
@@ -29,7 +29,7 @@ source:
|
||||
|
||||
## 사실
|
||||
|
||||
작업본 삭제 실패는 다섯 참조 중 무엇이 막았든 같은 오류 코드로 나간다 — `DOCUMENT_IN_USE`.
|
||||
작업본 삭제 실패는 다섯 참조 중 무엇이 막았든 같은 오류 코드로 나간다 — DOCUMENT_IN_USE.
|
||||
|
||||
클라이언트에 나가는 문구는 코드마다 하나로 고정돼 있다. 그 코드의 문구는 「이 기록을 참조하는 곳이 있어 삭제할 수 없습니다」이다.
|
||||
|
||||
@@ -37,15 +37,15 @@ source:
|
||||
|
||||
참조 검사는 다섯 표를 하나의 존재 검사로 묶는다.
|
||||
|
||||
```sql
|
||||
sql
|
||||
SELECT 1 FROM document_relation WHERE target_document_id = :id
|
||||
UNION ALL SELECT 1 FROM question_document_link WHERE document_id = :id
|
||||
UNION ALL SELECT 1 FROM project_document_link WHERE document_id = :id
|
||||
UNION ALL SELECT 1 FROM topic_featured_document WHERE document_id = :id
|
||||
UNION ALL SELECT 1 FROM project_decision WHERE source_case_id = :id
|
||||
```
|
||||
|
||||
같은 어댑터의 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다 — `project_question_link` 와 `home_focus_config.open_question_id` 다.
|
||||
|
||||
같은 어댑터의 질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다 — project_question_link 와 home_focus_config.open_question_id 다.
|
||||
|
||||
실제 사례에서 관계를 다 지워도 삭제가 안 됐다. 남아 있던 것은 프로젝트 링크 한 행이었고, 그 링크는 「관계」 편집기가 아니라 문서의 Project 필드가 만든다.
|
||||
|
||||
|
||||
+2
-2
@@ -33,11 +33,11 @@ source:
|
||||
|
||||
## 결론
|
||||
|
||||
프론트 이미지 빌드에 `RUNTIME_API_BASE_URL` 을 넘기지 않으면 기본값이 이미지에 굳는다. 그 기본값이 존재하지 않는 주소다.
|
||||
프론트 이미지 빌드에 RUNTIME_API_BASE_URL 을 넘기지 않으면 기본값이 이미지에 굳는다. 그 기본값이 존재하지 않는 주소다.
|
||||
|
||||
Dockerfile 이 그 경고를 문자 그대로 적어 두고 있는데도 빠뜨렸다.
|
||||
|
||||
`kubectl rollout undo` 로 되돌리고 다시 빌드했다.
|
||||
kubectl rollout undo 로 되돌리고 다시 빌드했다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ healthy 판정은 헬스 엔드포인트를 본다. 그 엔드포인트는 이
|
||||
|
||||
이미지가 권한을 정규화하도록 고쳤다.
|
||||
|
||||
같은 배포에서 favicon 도 404 였다. `index.html` 이 `public/favicon.svg` 를 참조한 적이 없다. 파일은 이미지에 들어 있었고 nginx 도 서빙했지만 브라우저는 `/favicon.ico` 를 물었고 404 를 받아 기본 아이콘으로 떨어졌다.
|
||||
같은 배포에서 favicon 도 404 였다. index.html 이 public/favicon.svg 를 참조한 적이 없다. 파일은 이미지에 들어 있었고 nginx 도 서빙했지만 브라우저는 /favicon.ico 를 물었고 404 를 받아 기본 아이콘으로 떨어졌다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
|
||||
+5
-5
@@ -29,13 +29,13 @@ source:
|
||||
|
||||
이 건은 같은 갈래의 다른 것들과 결이 다르다. 테스트가 아니라 생성기가 값을 버렸다.
|
||||
|
||||
파생 스펙을 파서에 넣으면 스키마 15개를 「is not of type `object`」로 거절했다. 그 스키마들은 전부 `type: object` 를 명시하고 있어서 계약 결함처럼 보이지 않았다.
|
||||
파생 스펙을 파서에 넣으면 스키마 15개를 「is not of type object」로 거절했다. 그 스키마들은 전부 type: object 를 명시하고 있어서 계약 결함처럼 보이지 않았다.
|
||||
|
||||
`validateSpec` 을 끄면 생성이 성공한다. 그렇게 만든 모델을 컴파일하면 통과한다.
|
||||
validateSpec 을 끄면 생성이 성공한다. 그렇게 만든 모델을 컴파일하면 통과한다.
|
||||
|
||||
## 결론
|
||||
|
||||
거절의 원인은 계약이 아니라 파생 단계였다. 변환들이 같은 `Map` 인스턴스를 여러 property 에 재사용했고 snakeyaml 이 그 지점을 anchor 와 alias 로 덤프했다. 파생 스펙에 alias 가 34곳 있었다.
|
||||
거절의 원인은 계약이 아니라 파생 단계였다. 변환들이 같은 Map 인스턴스를 여러 property 에 재사용했고 snakeyaml 이 그 지점을 anchor 와 alias 로 덤프했다. 파생 스펙에 alias 가 34곳 있었다.
|
||||
|
||||
검증을 끄고 만든 모델에서 사라진 필드 넷 :
|
||||
LatestEntry.publishedAt
|
||||
@@ -45,7 +45,7 @@ ReleaseListItem.changeTypes
|
||||
|
||||
컴파일이 통과한 이유는 아직 그 필드를 쓰는 코드가 없어서다.
|
||||
|
||||
덤프 직전 deep copy 로 노드 identity 를 끊어 alias 를 원천 차단하고, 남으면 빌드가 실패하도록 fail-closed 게이트를 뒀다. `validateSpec` 은 다시 켰다. 생성 모델 대조는 schema 이름에서 property 단위로 강화했다 — 이번 누락을 그 게이트가 통과시켰기 때문이다.
|
||||
덤프 직전 deep copy 로 노드 identity 를 끊어 alias 를 원천 차단하고, 남으면 빌드가 실패하도록 fail-closed 게이트를 뒀다. validateSpec 은 다시 켰다. 생성 모델 대조는 schema 이름에서 property 단위로 강화했다 — 이번 누락을 그 게이트가 통과시켰기 때문이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
@@ -57,7 +57,7 @@ tech-log-backend : 365560e
|
||||
## 재현 조건
|
||||
|
||||
1. 파생 스펙에서 anchor 와 alias 를 찾는다
|
||||
2. `validateSpec` 을 켜고 생성한다 — 그 스키마들이 거절된다
|
||||
2. validateSpec 을 켜고 생성한다 — 그 스키마들이 거절된다
|
||||
3. 끄고 생성한 뒤 모델의 property 를 계약과 하나씩 맞춘다
|
||||
|
||||
## 본문
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@ source:
|
||||
|
||||
# 그 SQL 은 한 번도 실행된 적이 없었다
|
||||
|
||||
작업본 삭제가 500 을 돌려줬다. 참조 검사가 없는 컬럼을 조회하고 있었다. 진짜 문제는 그 SQL 이 한 번도 실행된 적이 없다는 것이었다 — 표준 `check` 는 Testcontainers 를 띄우지 않으므로 persistence SQL 을 한 줄도 돌리지 않고 빌드가 통과한다.
|
||||
작업본 삭제가 500 을 돌려줬다. 참조 검사가 없는 컬럼을 조회하고 있었다. 진짜 문제는 그 SQL 이 한 번도 실행된 적이 없다는 것이었다 — 표준 check 는 Testcontainers 를 띄우지 않으므로 persistence SQL 을 한 줄도 돌리지 않고 빌드가 통과한다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -27,7 +27,7 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
작업본을 지우려 하면 500 이 났다. 참조 검사가 `public_resource_projection.document_id` 를 조회하는데 그런 컬럼이 없다.
|
||||
작업본을 지우려 하면 500 이 났다. 참조 검사가 public_resource_projection.document_id 를 조회하는데 그런 컬럼이 없다.
|
||||
|
||||
이 테이블은 하나로 case·question·project·release 를 모두 담기 때문에 종류와 식별자 두 컬럼으로 기록을 가리킨다.
|
||||
|
||||
@@ -37,7 +37,7 @@ source:
|
||||
|
||||
> 그 쿼리의 여섯 컬럼 중 다섯은 마이그레이션과 대조했다. 이 하나만 가정했고, 그것이 틀렸다.
|
||||
|
||||
표준 `check` 는 Testcontainers 를 띄우지 않는다. persistence SQL 은 한 번도 실행되지 않은 채 빌드가 통과하고, 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못한다.
|
||||
표준 check 는 Testcontainers 를 띄우지 않는다. persistence SQL 은 한 번도 실행되지 않은 채 빌드가 통과하고, 컴파일도 단위 테스트도 컬럼 이름을 검증하지 못한다.
|
||||
|
||||
삭제 경로 전용 통합 테스트 태스크를 만들고 실패했던 그 쿼리를 포함해 여덟 시나리오를 실제 PostgreSQL 에서 돌린다.
|
||||
|
||||
@@ -51,7 +51,7 @@ DB : 실제 PostgreSQL (Testcontainers)
|
||||
## 재현 조건
|
||||
|
||||
1. 어댑터의 SQL 에서 컬럼 이름을 하나 틀리게 적는다
|
||||
2. `./gradlew check` 를 돌린다 — 통과한다
|
||||
2. ./gradlew check 를 돌린다 — 통과한다
|
||||
3. 삭제 경로 통합 테스트 태스크를 돌린다 — 그 쿼리에서 멈춘다
|
||||
|
||||
## 본문
|
||||
|
||||
+6
-6
@@ -31,18 +31,18 @@ source:
|
||||
|
||||
컴포넌트 스캔이 생성자를 고르지 못하면 컨텍스트가 refresh 에 실패한다. 컨텍스트를 띄우는 테스트가 없으면 그 실패는 배포에서 처음 나타난다.
|
||||
|
||||
두 번째 사건은 import 였다. `JdbcProjectRepositoryAdapter` 가 Jackson 2 의 `ObjectMapper` 를 요구했는데 이 빌드는 Jackson 3 이다.
|
||||
두 번째 사건은 import 였다. JdbcProjectRepositoryAdapter 가 Jackson 2 의 ObjectMapper 를 요구했는데 이 빌드는 Jackson 3 이다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 건 다 컨텍스트가 뜰 때 처음 드러났다.
|
||||
|
||||
첫 번째 : 스캔되는 컴포넌트에 생성자 둘, `@Autowired` 없음
|
||||
두 번째 : Jackson 2 `ObjectMapper` 를 요구, 이 빌드는 Jackson 3
|
||||
첫 번째 : 스캔되는 컴포넌트에 생성자 둘, @Autowired 없음
|
||||
두 번째 : Jackson 2 ObjectMapper 를 요구, 이 빌드는 Jackson 3
|
||||
|
||||
두 번째가 컴파일을 통과한 이유는 Jackson 2 타입이 어떤 전이 의존성을 통해 클래스패스에 남아 있어서다. 잘못된 import 가 정상적으로 해석된다.
|
||||
|
||||
첫 번째는 ArchUnit 규칙으로 막았다. 스캔되는 컴포넌트는 생성자가 하나이거나, 여럿이면 그중 하나에 `@Autowired` 가 붙어야 한다. 규칙이 실제로 잡는지 결함을 되돌려 확인했다.
|
||||
첫 번째는 ArchUnit 규칙으로 막았다. 스캔되는 컴포넌트는 생성자가 하나이거나, 여럿이면 그중 하나에 @Autowired 가 붙어야 한다. 규칙이 실제로 잡는지 결함을 되돌려 확인했다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
@@ -53,8 +53,8 @@ Jackson : 이 빌드는 Jackson 3, 클래스패스에 Jackson 2 타입이 전이
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 스캔되는 컴포넌트에 생성자를 둘 만들고 `@Autowired` 를 붙이지 않는다
|
||||
2. `./gradlew check` 를 돌린다 — 통과한다
|
||||
1. 스캔되는 컴포넌트에 생성자를 둘 만들고 @Autowired 를 붙이지 않는다
|
||||
2. ./gradlew check 를 돌린다 — 통과한다
|
||||
3. ArchUnit D20 규칙을 켠 상태로 돌린다 — 그 컴포넌트를 짚는다
|
||||
|
||||
## 본문
|
||||
|
||||
@@ -1683,16 +1683,8 @@
|
||||
"file": "an-axis-inside-a-topic/case/case-the-comparison-band-changed-three-times.md",
|
||||
"status": "게시 전",
|
||||
"studioId": "",
|
||||
"assets": [
|
||||
"home-tabs-keycloak",
|
||||
"home-topic-tabs-2",
|
||||
"home-tabs-grouped"
|
||||
],
|
||||
"assetFiles": [
|
||||
"home-tabs-keycloak.png",
|
||||
"home-topic-tabs-2.png",
|
||||
"home-tabs-grouped.png"
|
||||
],
|
||||
"assets": [],
|
||||
"assetFiles": [],
|
||||
"evidenceFiles": [
|
||||
"../../../final/evidence/browser/home-tabs-keycloak.png",
|
||||
"../../../final/evidence/browser/home-topic-tabs.png",
|
||||
|
||||
+2
-2
@@ -27,9 +27,9 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
결정은 상세 endpoint 가 없다. 공개 주소가 `/projects/{slug}/decisions#{slug}` 로 목록 위의 앵커다.
|
||||
결정은 상세 endpoint 가 없다. 공개 주소가 /projects/{slug}/decisions#{slug} 로 목록 위의 앵커다.
|
||||
|
||||
상세가 없으면 화면이 그리는 칸이 전부 목록 항목에 있어야 한다. 목록 항목에는 `title`·`summary`·`consequences`·`evidence` 가 빠져 있었다.
|
||||
상세가 없으면 화면이 그리는 칸이 전부 목록 항목에 있어야 한다. 목록 항목에는 title·summary·consequences·evidence 가 빠져 있었다.
|
||||
|
||||
## 결론
|
||||
|
||||
|
||||
+6
-6
@@ -15,7 +15,7 @@ source:
|
||||
|
||||
# 공개 Reference 가 통째로 비어 있었다 — 이름이 어긋났고 본문은 다른 테이블에 있었다
|
||||
|
||||
Reference 를 게시했더니 Studio 에서는 모든 칸이 보이는데 공개 화면만 통째로 비어 있었다. 원인이 둘 겹쳐 있었다. 게이트웨이가 읽던 칸 이름이 계약에 없는 것들이었고, Reference 의 본문이 `body_markdown` 이 아니라 별도 테이블에 있었다. 타입 검사는 `as` 단언 때문에 아무 말도 하지 않았다.
|
||||
Reference 를 게시했더니 Studio 에서는 모든 칸이 보이는데 공개 화면만 통째로 비어 있었다. 원인이 둘 겹쳐 있었다. 게이트웨이가 읽던 칸 이름이 계약에 없는 것들이었고, Reference 의 본문이 body_markdown 이 아니라 별도 테이블에 있었다. 타입 검사는 as 단언 때문에 아무 말도 하지 않았다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -24,7 +24,7 @@ Reference 를 게시했더니 Studio 에서는 모든 칸이 보이는데 공개
|
||||
- **Studio 에서는 보이는데 공개 쪽만 비면 그 사이에 계약이 있다**
|
||||
이 사건에서 굳힌 진단 규칙이다.
|
||||
- **TypeScript 가 검사를 놓아 주는 네 곳**
|
||||
`as` 단언이 어긋남을 가린 것을 그 개념이 설명한다.
|
||||
as 단언이 어긋남을 가린 것을 그 개념이 설명한다.
|
||||
|
||||
## 문제
|
||||
|
||||
@@ -40,9 +40,9 @@ DB 에는 작성자가 쓴 값이 그대로 있었다. 두 화면이 같은 데
|
||||
계약이 주는 이름 : scopeSummary · appliesTo · excludedScope
|
||||
결과 : 전부 undefined 로 떨어졌고, as string 단언 때문에 타입 검사가 통과했다
|
||||
|
||||
Reference 의 본문은 `body_markdown` 이 아니라 `reference_detail` 의 규칙과 예시에 있다. Studio 편집기가 규칙을 제목과 본문으로 나눠 받고 마크다운 본문은 비워 두기 때문이다. 공개 조회는 `body_markdown` 만 보고 빈 문자열을 내보냈다.
|
||||
Reference 의 본문은 body_markdown 이 아니라 reference_detail 의 규칙과 예시에 있다. Studio 편집기가 규칙을 제목과 본문으로 나눠 받고 마크다운 본문은 비워 두기 때문이다. 공개 조회는 body_markdown 만 보고 빈 문자열을 내보냈다.
|
||||
|
||||
고친 뒤에는 값이 아니라 이름을 지키는 테스트를 뒀다. 계약에서 그 칸이 사라지면 `satisfies` 가 먼저 깨진다. 값을 검사하는 테스트로는 이 결함이 잡히지 않는다.
|
||||
고친 뒤에는 값이 아니라 이름을 지키는 테스트를 뒀다. 계약에서 그 칸이 사라지면 satisfies 가 먼저 깨진다. 값을 검사하는 테스트로는 이 결함이 잡히지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
@@ -69,7 +69,7 @@ tech-log-backend : a5f93b9 이후
|
||||
const summary = body.purposeSummary as string; // 계약에 그런 칸이 없다
|
||||
```
|
||||
|
||||
`as` 는 「이 값을 이 타입으로 다루겠다」는 선언이므로, 컴파일러는 그 이름이 응답 타입에 있는지 묻지 않는다. 실행하면 `undefined` 가 나오고 화면은 빈 문자열을 그린다.
|
||||
as 는 「이 값을 이 타입으로 다루겠다」는 선언이므로, 컴파일러는 그 이름이 응답 타입에 있는지 묻지 않는다. 실행하면 `undefined` 가 나오고 화면은 빈 문자열을 그린다.
|
||||
|
||||
| 게이트웨이가 읽던 이름 | 계약이 주는 이름 |
|
||||
|---|---|
|
||||
@@ -111,6 +111,6 @@ Reference 의 본문은 문서 본문 칸이 아니라 규칙과 예시를 담
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
`as` 단언을 걷어낸 것은 이 매퍼 하나다. 같은 모양이 다른 매퍼에 남아 있는지 전수로 세지 않았다.
|
||||
as 단언을 걷어낸 것은 이 매퍼 하나다. 같은 모양이 다른 매퍼에 남아 있는지 전수로 세지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
|
||||
+2
-2
@@ -40,9 +40,9 @@ flattenRelations : 담지 않음 — 1차로 고침
|
||||
렌더 모델로 변환 : 담을 칸 자체가 없었음
|
||||
화면 목록으로 전달 : 또 버림
|
||||
|
||||
렌더 모델 계약(`ResolvedRelation`)에 요약 칸이 없었고 `additionalProperties: false` 라 실을 수도 없었다. 계약에 `summary` 를 더하고 — 이미 나가 있는 응답을 깨지 않으려고 required 에는 넣지 않고 — 세 경계를 모두 이었다.
|
||||
렌더 모델 계약(ResolvedRelation)에 요약 칸이 없었고 additionalProperties: false 라 실을 수도 없었다. 계약에 summary 를 더하고 — 이미 나가 있는 응답을 깨지 않으려고 required 에는 넣지 않고 — 세 경계를 모두 이었다.
|
||||
|
||||
그 과정에서 한 칸에 뭉쳐 있던 셋을 갈랐다. 대상의 종류는 `label`, 작성자가 쓴 이유는 `note`, 대상의 요약은 `summary` 다.
|
||||
그 과정에서 한 칸에 뭉쳐 있던 셋을 갈랐다. 대상의 종류는 label, 작성자가 쓴 이유는 note, 대상의 요약은 summary 다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
|
||||
+3
-4
@@ -18,7 +18,7 @@ source:
|
||||
|
||||
# 공개 화면 한 줄이 그려지기까지 값이 지나는 경계 열한 개
|
||||
|
||||
공개 화면의 한 줄은 PostgreSQL 의 투영 테이블에서 출발해 열한 번 모양을 바꾼 뒤에 그려진다. 그 사이 어느 한 곳이 값을 담지 않아도 오류가 나지 않는다. `undefined` 는 빈 문자열로 그려지고 빈 배열은 「항목이 없습니다」로 그려진다.
|
||||
공개 화면의 한 줄은 PostgreSQL 의 투영 테이블에서 출발해 열한 번 모양을 바꾼 뒤에 그려진다. 그 사이 어느 한 곳이 값을 담지 않아도 오류가 나지 않는다. undefined 는 빈 문자열로 그려지고 빈 배열은 「항목이 없습니다」로 그려진다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -51,8 +51,7 @@ PostgreSQL 테이블
|
||||
└─ 화면 컴포넌트
|
||||
```
|
||||
|
||||
:::evidence key="value-boundaries" alt="저장·백엔드 조립·HTTP envelope·프론트엔드 조립·화면 다섯 묶음을 세 저장소 구역으로 나눠 이은 흐름도" caption=" " zoom="true"
|
||||
:::
|
||||

|
||||
|
||||
저장 쪽에 둘, 백엔드 조립에 넷, 전선에 하나, 프론트엔드 조립에 셋, 화면에 하나다. 저장소 경계로 보면 백엔드가 여섯, 전선이 하나, 프론트엔드가 넷이다.
|
||||
|
||||
@@ -64,7 +63,7 @@ PostgreSQL 테이블
|
||||
|---|---|---|
|
||||
| 어댑터 SQL | 실행할 때 컬럼이 있는지 | 컴파일 시점에는 컬럼 이름을 아무도 안 본다 |
|
||||
| 생성된 DTO | 계약의 스키마 모양 | 그 칸에 값이 담겼는지 |
|
||||
| 게이트웨이 매퍼 | 계약이 준 타입의 이름 | `as` 단언을 쓰면 그 확인이 사라진다 |
|
||||
| 게이트웨이 매퍼 | 계약이 준 타입의 이름 | as 단언을 쓰면 그 확인이 사라진다 |
|
||||
| 포트와 화면 | 두 타입이 맞는지 | 포트와 어댑터가 타입을 따로 들면 한쪽만 늘어난다 |
|
||||
|
||||
어댑터 SQL 은 컬럼 이름을 문자열로 적는다. 이름이 틀리면 실행할 때 알게 되고, 그 SQL 을 실제로 돌리는 검사가 없으면 배포 뒤에 알게 된다.
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ Studio 편집기에서는 값이 다 보이는데 공개 화면만 비어 있으
|
||||
|
||||
데이터베이스에 값이 있는데 화면이 비어 있을 때 어디를 먼저 볼지 정한다.
|
||||
|
||||
이 부류는 오류를 내지 않으므로 로그에서 출발하면 아무것도 나오지 않는다. `undefined` 는 빈 문자열로 그려지고 빈 배열은 「항목이 없습니다」로 그려진다.
|
||||
이 부류는 오류를 내지 않으므로 로그에서 출발하면 아무것도 나오지 않는다. undefined 는 빈 문자열로 그려지고 빈 배열은 「항목이 없습니다」로 그려진다.
|
||||
|
||||
## 규칙
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ source:
|
||||
|
||||
### 2. 타입 검사 통과를 반영의 증거로 쓰지 않는다
|
||||
|
||||
메서드 매개변수의 bivariance, `as` 단언, 검사 대상이 없는 tsconfig 가 각각 통과시킨 사례가 있다. 통과는 「코드가 맞다」가 아니라 「검사가 그 질문을 하지 않았다」를 뜻할 수 있다.
|
||||
메서드 매개변수의 bivariance, as 단언, 검사 대상이 없는 tsconfig 가 각각 통과시킨 사례가 있다. 통과는 「코드가 맞다」가 아니라 「검사가 그 질문을 하지 않았다」를 뜻할 수 있다.
|
||||
|
||||
### 3. 게이트웨이를 실제로 불러 어떤 연산이 나가는지 확인한다
|
||||
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ source:
|
||||
|
||||
포트는 네 종류를 받는다고 선언돼 있는데 구현은 세 종류만 적혀 있었다. 그 상태로 타입 검사가 통과했다.
|
||||
|
||||
CONCEPT 을 넘기면 구현의 삼항 사슬이 마지막 `else` 로 떨어뜨려 질문 삭제 경로를 부른다. 배포된 번들에서 서버 로그에 `DELETE /api/v1/studio/questions/{id} 404` 가 계속 찍혔다.
|
||||
CONCEPT 을 넘기면 구현의 삼항 사슬이 마지막 else 로 떨어뜨려 질문 삭제 경로를 부른다. 배포된 번들에서 서버 로그에 DELETE /api/v1/studio/questions/{id} 404 가 계속 찍혔다.
|
||||
|
||||
## 결론
|
||||
|
||||
@@ -40,7 +40,7 @@ TypeScript 에서 메서드 매개변수는 bivariant 다. 구현이 매개변
|
||||
타입 검사 : 통과
|
||||
실행 결과 : CONCEPT 이 질문 삭제로 나감
|
||||
|
||||
같은 병이 필터 타입에서도 났다. 포트와 정적 어댑터가 타입을 따로 들고 있어, 포트에 필터가 늘어도 어댑터는 모르는 상태가 됐다. `satisfies` 도 같은 이유로 잡지 못했다. 타입을 하나로 합쳐서 고쳤다.
|
||||
같은 병이 필터 타입에서도 났다. 포트와 정적 어댑터가 타입을 따로 들고 있어, 포트에 필터가 늘어도 어댑터는 모르는 상태가 됐다. satisfies 도 같은 이유로 잡지 못했다. 타입을 하나로 합쳐서 고쳤다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
|
||||
+6
-6
@@ -14,7 +14,7 @@ source:
|
||||
|
||||
# npx tsc --noEmit 이 한 파일도 검사하지 않고 성공했다
|
||||
|
||||
운영에서 릴리즈 목록이 `ReferenceError` 로 비었다. import 하나가 빠져 있었고 다른 변수는 아예 정의된 적이 없었다. `npx tsc --noEmit` 이 통과했기 때문에 그것을 보지 못했다. 루트 tsconfig 는 `"files": []` 에 project references 만 나열하므로 그 명령은 한 파일도 검사하지 않고 성공한다.
|
||||
운영에서 릴리즈 목록이 ReferenceError 로 비었다. import 하나가 빠져 있었고 다른 변수는 아예 정의된 적이 없었다. npx tsc --noEmit 이 통과했기 때문에 그것을 보지 못했다. 루트 tsconfig 는 "files": [] 에 project references 만 나열하므로 그 명령은 한 파일도 검사하지 않고 성공한다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -27,15 +27,15 @@ source:
|
||||
|
||||
## 문제
|
||||
|
||||
운영에서 릴리즈 목록 화면이 비었고 콘솔에 `ReferenceError` 가 났다. 링크 컴포넌트 import 가 빠졌고, 이동 함수는 정의된 적이 없었다.
|
||||
운영에서 릴리즈 목록 화면이 비었고 콘솔에 ReferenceError 가 났다. 링크 컴포넌트 import 가 빠졌고, 이동 함수는 정의된 적이 없었다.
|
||||
|
||||
타입 검사를 돌렸을 때 통과했다. 그래서 이 오류가 배포까지 갔다.
|
||||
|
||||
## 결론
|
||||
|
||||
`npx tsc --noEmit` 은 루트 tsconfig 를 읽는다. 그 파일은 `"files": []` 에 project references 만 나열하므로 검사할 파일이 없고, 없는 채로 성공한다.
|
||||
npx tsc --noEmit 은 루트 tsconfig 를 읽는다. 그 파일은 "files": [] 에 project references 만 나열하므로 검사할 파일이 없고, 없는 채로 성공한다.
|
||||
|
||||
실제 검사는 `npm run check:types` 가 한다. 이 명령이 여섯 개 프로젝트를 돌며 검사한다.
|
||||
실제 검사는 npm run check:types 가 한다. 이 명령이 여섯 개 프로젝트를 돌며 검사한다.
|
||||
|
||||
그 명령으로 돌리자 저장소에 남아 있던 다른 오류도 함께 드러났다.
|
||||
|
||||
@@ -53,8 +53,8 @@ tsconfig : 루트가 project references 만 나열
|
||||
## 재현 조건
|
||||
|
||||
1. 어느 프로젝트 파일에 정의되지 않은 변수를 하나 넣는다
|
||||
2. `npx tsc --noEmit` 을 돌린다 — 성공한다
|
||||
3. `npm run check:types` 를 돌린다 — 그 파일에서 멈춘다
|
||||
2. npx tsc --noEmit 을 돌린다 — 성공한다
|
||||
3. npm run check:types 를 돌린다 — 그 파일에서 멈춘다
|
||||
|
||||
## 본문
|
||||
|
||||
|
||||
+5
-5
@@ -16,7 +16,7 @@ source:
|
||||
|
||||
# TypeScript 가 검사를 놓아 주는 네 자리 — 메서드 매개변수의 bivariance · as 단언 · never 캐스트 · 검사 대상을 갖지 않은 tsconfig
|
||||
|
||||
「타입 검사가 통과했으니 반영됐다」는 판단이 이 저장소에서 네 번 틀렸다. 매번 다른 이유였다 — 메서드 매개변수의 bivariance, `as` 단언, `never` 로 받아 캐스팅하는 조립기, 그리고 검사할 파일을 갖지 않은 tsconfig 다.
|
||||
「타입 검사가 통과했으니 반영됐다」는 판단이 이 저장소에서 네 번 틀렸다. 매번 다른 이유였다 — 메서드 매개변수의 bivariance, as 단언, never 로 받아 캐스팅하는 조립기, 그리고 검사할 파일을 갖지 않은 tsconfig 다.
|
||||
|
||||
## 관계
|
||||
|
||||
@@ -25,7 +25,7 @@ source:
|
||||
- **npx tsc --noEmit 이 한 파일도 검사하지 않고 성공했다**
|
||||
검사 대상이 없는 tsconfig 가 통과시킨 사건이다.
|
||||
- **공개 Reference 가 통째로 비어 있었다**
|
||||
`as` 단언이 통과시킨 사건이다.
|
||||
as 단언이 통과시킨 사건이다.
|
||||
|
||||
## 본문
|
||||
|
||||
@@ -35,7 +35,7 @@ source:
|
||||
|
||||
「타입 검사가 통과했다」가 뜻하는 것은 컴파일러가 물은 질문에 코드가 답했다는 것이다. 이 저장소에서 네 번, 컴파일러가 물었어야 할 질문을 묻지 않았다.
|
||||
|
||||
넷은 서로 다른 방식으로 질문을 바꾼다. bivariance 는 「이 구현이 그 포트와 맞는가」를 「두 시그니처가 호환되는가」로, `as` 는 「이 이름이 그 타입에 있는가」를 작성자의 선언으로, `never` 캐스트는 「이 인자를 다 적었는가」를 검사 없음으로, 검사 대상이 없는 tsconfig 는 「이 코드가 컴파일되는가」를 대상 없음으로 바꾼다.
|
||||
넷은 서로 다른 방식으로 질문을 바꾼다. bivariance 는 「이 구현이 그 포트와 맞는가」를 「두 시그니처가 호환되는가」로, as 는 「이 이름이 그 타입에 있는가」를 작성자의 선언으로, `never` 캐스트는 「이 인자를 다 적었는가」를 검사 없음으로, 검사 대상이 없는 tsconfig 는 「이 코드가 컴파일되는가」를 대상 없음으로 바꾼다.
|
||||
|
||||
## 메서드 매개변수는 bivariant 다
|
||||
|
||||
@@ -57,7 +57,7 @@ deleteDocument(kind: "CASE" | "REFERENCE" | "QUESTION" | "CONCEPT", id: string):
|
||||
const summary = body.purposeSummary as string; // 계약에 그런 칸이 없다
|
||||
```
|
||||
|
||||
`as` 는 「이 값을 이 타입으로 다루겠다」는 선언이다. 그 이름이 응답 타입에 있는지를 컴파일러가 묻지 않고, 실행하면 `undefined` 가 나온다.
|
||||
as 는 「이 값을 이 타입으로 다루겠다」는 선언이다. 그 이름이 응답 타입에 있는지를 컴파일러가 묻지 않고, 실행하면 `undefined` 가 나온다.
|
||||
|
||||
모양이 다른 경우에는 더 나빠진다. 객체를 배열로 읽고 `.filter` 를 부르면 매핑이 통째로 터지는데, 캐스트가 그 어긋남을 타입 검사에서 가리므로 배포된 뒤에 알게 된다.
|
||||
|
||||
@@ -78,7 +78,7 @@ const summary = body.purposeSummary as string; // 계약에 그런 칸이 없
|
||||
| 무엇이 통과했나 | 무엇이 확인되지 않았나 | 어디서 드러났나 |
|
||||
|---|---|---|
|
||||
| bivariance | 구현이 네 번째 값을 다루는가 | 배포본의 서버 로그 404 |
|
||||
| `as` 단언 | 그 이름이 응답 타입에 있는가 | 공개 화면이 비어 있음 |
|
||||
| as 단언 | 그 이름이 응답 타입에 있는가 | 공개 화면이 비어 있음 |
|
||||
| `never` 캐스트 | 새 질의 인자를 조립기가 적었는가 | 페이지 번호가 안 넘어감 |
|
||||
| 검사 대상 없는 tsconfig | 이 코드가 컴파일되는가 | 운영의 ReferenceError |
|
||||
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ CI 에 묶이지 않은 검증이 남아 있으면 그것을 돌리는 것은
|
||||
|
||||
### 2. 타입 검사는 프로젝트를 순회하는 명령으로 돌린다
|
||||
|
||||
루트 tsconfig 를 직접 부르는 명령은 한 파일도 검사하지 않고 성공한다. 루트가 `"files": []` 에 project references 만 나열하기 때문이다.
|
||||
루트 tsconfig 를 직접 부르는 명령은 한 파일도 검사하지 않고 성공한다. 루트가 "files": [] 에 project references 만 나열하기 때문이다.
|
||||
|
||||
### 3. 백엔드는 커밋한 뒤에 빌드한다
|
||||
|
||||
@@ -51,7 +51,7 @@ CI 에 묶이지 않은 검증이 남아 있으면 그것을 돌리는 것은
|
||||
|
||||
### 4. 테스트를 npm 이나 npx 로 감싸 돌리지 않는다
|
||||
|
||||
`npm_config_*` 환경 변수가 설정되어 CI 워크플로 생성 테스트가 실패한다. 그 변수를 지우고 실행기를 직접 부른다.
|
||||
npm_config_* 환경 변수가 설정되어 CI 워크플로 생성 테스트가 실패한다. 그 변수를 지우고 실행기를 직접 부른다.
|
||||
|
||||
### 5. 환경 때문에 실패하는 것은 실패로 세지 않되 목록에 적는다
|
||||
|
||||
|
||||
Reference in New Issue
Block a user