From a5f93b9b75b6031fd1d11ab90e62cc986a6dbfbb Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 25 Aug 2026 19:11:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B3=B5=EA=B0=9C=20Reference=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20=ED=8C=90=EB=8B=A8=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EA=B3=BC=20=EC=98=88=EC=8B=9C=EB=A5=BC=20=EC=8B=A3?= =?UTF-8?q?=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference 의 본문은 `body_markdown` 이 아니라 `reference_detail.rules` 와 `examples` 에 있다. Studio 의 Reference 편집기가 규칙(제목+본문)과 예시를 따로 받고 마크다운 본문은 비워 두기 때문이다. 공개 조회는 그 두 칸을 읽지 않고 `body_markdown` 만 봤다. 그래서 `content: ""` 를 내보냈고, 공개 화면의 "판단 기준"과 "예시"가 통째로 비었다 — Studio 에서는 다 보이는데 공개 쪽만 빈 이유가 이것이다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- .../mapper/DocumentResponseMapper.java | 8 ++++++++ .../JdbcPublicDocumentQueryAdapter.java | 4 ++++ .../techlog/publicsite/PublicJson.java | 11 +++++++++++ .../publicsite/model/PublishedDocumentView.java | 4 ++++ .../publicsite/model/ReferenceRuleView.java | 8 ++++++++ src/config/openapi/MANIFEST.sha256 | 8 ++++---- src/config/openapi/public-v1.yaml | 17 +++++++++++++++++ 7 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/ReferenceRuleView.java diff --git a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/publicapi/mapper/DocumentResponseMapper.java b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/publicapi/mapper/DocumentResponseMapper.java index 9c9988b..311caf4 100644 --- a/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/publicapi/mapper/DocumentResponseMapper.java +++ b/src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/techlog/publicapi/mapper/DocumentResponseMapper.java @@ -11,6 +11,7 @@ import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.QuestionPo import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.QuestionUpdatePublic; import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ReferenceDetailResponse; import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ReferenceDetailResponseReference; +import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ReferenceDetailResponseReferenceRulesInner; import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ReferenceDetailResponseRelations; import dev.caskeleton.application.techlog.publicsite.model.CaseDetailView; import dev.caskeleton.application.techlog.publicsite.model.PublishedDocumentView; @@ -74,6 +75,13 @@ public final class DocumentResponseMapper { body.setScopeSummary(doc.primarySummary()); body.setAppliesTo(doc.appliesTo()); body.setExcludedScope(doc.excludedScope()); + // Reference 의 본문은 `content` 가 아니라 여기 있다 — 이것을 빼면 공개 화면에 판단 기준과 + // 예시가 통째로 빠진다. + body.setRules( + doc.rules().stream() + .map(rule -> new ReferenceDetailResponseReferenceRulesInner(rule.title(), rule.body())) + .toList()); + body.setExamples(doc.examples()); body.setFreshnessStatus( ReferenceDetailResponseReference.FreshnessStatusEnum.fromValue(doc.freshnessStatus())); body.setContent(doc.content()); diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicDocumentQueryAdapter.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicDocumentQueryAdapter.java index e6ba20a..837d416 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicDocumentQueryAdapter.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicDocumentQueryAdapter.java @@ -97,6 +97,7 @@ public class JdbcPublicDocumentQueryAdapter implements PublicDocumentQueryPort { // 컬럼(`public_resource_projection.last_verified_at`)은 `upsertProjection` 이 // 채우지 않아 언제나 null 이었다. + " p.navigation_path, p.published_at, p.updated_at, d.last_verified_at," + + " r.rules, r.examples," + " t.name AS topic_name, t.slug AS topic_slug," + " pr.name AS project_name, pr.slug AS project_slug," + " a.content_type AS cover_content_type, a.alt_text AS cover_alt," @@ -132,6 +133,9 @@ public class JdbcPublicDocumentQueryAdapter implements PublicDocumentQueryPort { isCase ? environment(rs) : List.of(), isCase ? List.of() : json.strings(rs.getString("applies_to")), isCase ? List.of() : json.strings(rs.getString("excluded_scope")), + // Reference 의 본문은 body_markdown 이 아니라 규칙과 예시에 있다. + isCase ? List.of() : json.referenceRules(rs.getString("rules")), + isCase ? List.of() : json.strings(rs.getString("examples")), isCase ? null : rs.getString("freshness_status"), rs.getString("body_markdown"), rs.getString("content_format"), diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicJson.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicJson.java index 9c4bbc4..0e96431 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicJson.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicJson.java @@ -2,6 +2,7 @@ package dev.caskeleton.adapter.outbound.persistence.techlog.publicsite; import dev.caskeleton.application.techlog.publicsite.model.ContactLinkView; import dev.caskeleton.application.techlog.publicsite.model.ProfileView; +import dev.caskeleton.application.techlog.publicsite.model.ReferenceRuleView; import dev.caskeleton.shared.error.MappingException; import java.util.ArrayList; import java.util.List; @@ -32,6 +33,16 @@ final class PublicJson { return out; } + /** Reference 의 판단 기준. 설계의 컬럼은 {@code {title, body, order}} 배열이다. */ + List referenceRules(String json) { + List out = new ArrayList<>(); + for (JsonNode node : array(json)) { + out.add( + new ReferenceRuleView(node.path("title").asString(""), node.path("body").asString(""))); + } + return out; + } + List contacts(String json) { List out = new ArrayList<>(); for (JsonNode node : array(json)) { diff --git a/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/PublishedDocumentView.java b/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/PublishedDocumentView.java index 5eb9068..62f9095 100644 --- a/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/PublishedDocumentView.java +++ b/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/PublishedDocumentView.java @@ -20,6 +20,8 @@ public record PublishedDocumentView( List environmentSummary, List appliesTo, List excludedScope, + List rules, + List examples, String freshnessStatus, String content, String contentFormat, @@ -37,6 +39,8 @@ public record PublishedDocumentView( environmentSummary = environmentSummary == null ? List.of() : List.copyOf(environmentSummary); appliesTo = appliesTo == null ? List.of() : List.copyOf(appliesTo); excludedScope = excludedScope == null ? List.of() : List.copyOf(excludedScope); + rules = rules == null ? List.of() : List.copyOf(rules); + examples = examples == null ? List.of() : List.copyOf(examples); tags = tags == null ? List.of() : List.copyOf(tags); // Reference 에는 evidence 를 담는 본문이 없다. 빈 목록이 정상이며 없음과 구분하지 않는다. bodyAssets = bodyAssets == null ? List.of() : List.copyOf(bodyAssets); diff --git a/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/ReferenceRuleView.java b/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/ReferenceRuleView.java new file mode 100644 index 0000000..8e5debf --- /dev/null +++ b/src/application-core/src/main/java/dev/caskeleton/application/techlog/publicsite/model/ReferenceRuleView.java @@ -0,0 +1,8 @@ +package dev.caskeleton.application.techlog.publicsite.model; + +/** + * Reference 의 판단 기준 한 줄. 계약 {@code ReferenceDetailResponse.reference.rules[]}. + * + *

Reference 의 본문은 마크다운 한 덩어리가 아니라 제목이 붙은 규칙의 목록이다 — Studio 의 편집기가 그렇게 받고, 공개 화면도 그렇게 그린다. + */ +public record ReferenceRuleView(String title, String body) {} diff --git a/src/config/openapi/MANIFEST.sha256 b/src/config/openapi/MANIFEST.sha256 index 7bc41ba..676d18b 100644 --- a/src/config/openapi/MANIFEST.sha256 +++ b/src/config/openapi/MANIFEST.sha256 @@ -1,6 +1,6 @@ -# source: tech-log-design-package contracts/openapi/studio-v1.yaml @ 83148b2 (master) +# source: tech-log-design-package contracts/openapi/studio-v1.yaml @ ff0c12a (master) 18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd studio-v1.yaml -# source: tech-log-design-package contracts/openapi/public-v1.yaml @ 83148b2 (master) -702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c public-v1.yaml -# source: tech-log-design-package contracts/openapi/studio-management-v1.yaml @ 83148b2 (master) +# source: tech-log-design-package contracts/openapi/public-v1.yaml @ ff0c12a (master) +34efa8d2fdba959373081e5b2aace252be86bef243a80c238d8c4af50fa8eb0a public-v1.yaml +# source: tech-log-design-package contracts/openapi/studio-management-v1.yaml @ ff0c12a (master) 72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55 studio-management-v1.yaml diff --git a/src/config/openapi/public-v1.yaml b/src/config/openapi/public-v1.yaml index f40b592..bc34691 100644 --- a/src/config/openapi/public-v1.yaml +++ b/src/config/openapi/public-v1.yaml @@ -1335,6 +1335,23 @@ components: type: array items: type: string + # Reference 의 본문은 `content` 마크다운이 아니라 이 두 칸에 있다. Studio 의 Reference + # 편집기는 규칙(제목+본문)과 예시를 따로 받고 body_markdown 은 비워 두므로, 이것을 + # 내보내지 않으면 공개 화면에 판단 기준과 예시가 통째로 빠진다. + rules: + type: array + items: + type: object + required: [title, body] + properties: + title: + type: string + body: + type: string + examples: + type: array + items: + type: string freshnessStatus: type: string enum: