Compare commits
3
Commits
f1fd56fcb5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edb0890dc8 | ||
|
|
c6d9d2d675 | ||
|
|
a5f93b9b75 |
+10
@@ -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;
|
||||
@@ -36,6 +37,7 @@ public final class DocumentResponseMapper {
|
||||
PublishedDocumentView doc = view.document();
|
||||
CaseDetailResponseCase body = new CaseDetailResponseCase();
|
||||
body.setTitle(doc.title());
|
||||
body.setSummary(doc.summary());
|
||||
body.setProblemSummary(doc.primarySummary());
|
||||
body.setConclusionSummary(doc.secondarySummary());
|
||||
body.setEnvironmentSummary(doc.environmentSummary());
|
||||
@@ -71,9 +73,17 @@ public final class DocumentResponseMapper {
|
||||
PublishedDocumentView doc = view.document();
|
||||
ReferenceDetailResponseReference body = new ReferenceDetailResponseReference();
|
||||
body.setTitle(doc.title());
|
||||
body.setSummary(doc.summary());
|
||||
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());
|
||||
|
||||
+6
@@ -97,6 +97,8 @@ 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,"
|
||||
+ " d.summary,"
|
||||
+ " 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,"
|
||||
@@ -126,12 +128,16 @@ public class JdbcPublicDocumentQueryAdapter implements PublicDocumentQueryPort {
|
||||
type,
|
||||
rs.getString("navigation_path"),
|
||||
rs.getString("title"),
|
||||
rs.getString("summary"),
|
||||
// Case 는 문제/결론, Reference 는 범위/적용이 각각 앞뒤 요약 자리에 온다.
|
||||
isCase ? rs.getString("problem_summary") : rs.getString("scope_summary"),
|
||||
isCase ? rs.getString("conclusion_summary") : null,
|
||||
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"),
|
||||
|
||||
+2
-2
@@ -180,8 +180,8 @@ public class JdbcPublicSiteQueryAdapter implements PublicSiteQueryPort {
|
||||
}
|
||||
|
||||
/**
|
||||
* 계약 {@code LatestEntry.entryType} 은 {@code CASE / REFERENCE / PROJECT_ACTIVITY / RELEASE} 네 값만
|
||||
* 허용한다. projection 에는 {@code QUESTION}·{@code PROJECT}·{@code PROJECT_DECISION}·{@code PROFILE} 도
|
||||
* 계약 {@code LatestEntry.entryType} 은 {@code CASE / REFERENCE / QUESTION / PROJECT_ACTIVITY /
|
||||
* RELEASE} 다섯 값을 허용한다. projection 에는 {@code PROJECT}·{@code PROJECT_DECISION}·{@code PROFILE} 도
|
||||
* 들어 있으므로 여기서 걸러야 한다 — 거르지 않으면 응답 매퍼가 계약 밖 값을 만나 500 이 되고, 그 500 은 홈 화면 전체를 못 쓰게 만든다.
|
||||
*
|
||||
* <p>{@code RELEASE} 가 결과에 없는 것은 누락이 아니다. 릴리스는 Publication 파이프라인을 거치지 않고 자체 {@code
|
||||
|
||||
+11
@@ -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<ReferenceRuleView> referenceRules(String json) {
|
||||
List<ReferenceRuleView> out = new ArrayList<>();
|
||||
for (JsonNode node : array(json)) {
|
||||
out.add(
|
||||
new ReferenceRuleView(node.path("title").asString(""), node.path("body").asString("")));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
List<ContactLinkView> contacts(String json) {
|
||||
List<ContactLinkView> out = new ArrayList<>();
|
||||
for (JsonNode node : array(json)) {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ final class PublicSql {
|
||||
* 조건도 한 곳에서 정의한다.
|
||||
*/
|
||||
static final String LATEST_ENTRY_TYPES =
|
||||
" p.resource_type IN ('CASE', 'REFERENCE', 'PROJECT_ACTIVITY') ";
|
||||
" p.resource_type IN ('CASE', 'REFERENCE', 'QUESTION', 'PROJECT_ACTIVITY') ";
|
||||
|
||||
private PublicSql() {}
|
||||
|
||||
|
||||
+20
-5
@@ -206,12 +206,21 @@ class PublicSitePersistenceIntegrationTest {
|
||||
.noneMatch(entry -> entry.title().contains("숨김"));
|
||||
assertThat(view.latestEntries())
|
||||
.as(
|
||||
"계약 LatestEntry.entryType 은 네 값만 허용한다 — projection 의 QUESTION/PROJECT 등이 섞이면"
|
||||
+ " 응답 매퍼가 계약 밖 값을 만나 500 이 된다")
|
||||
"계약 LatestEntry.entryType 이 허용하는 값만 나와야 한다 — projection 의 PROJECT/PROJECT_DECISION"
|
||||
+ " 등이 섞이면 응답 매퍼가 계약 밖 값을 만나 500 이 되고, 그 500 은 홈 화면 전체를 못 쓰게 만든다")
|
||||
.extracting("entryType")
|
||||
.containsAnyOf("CASE", "REFERENCE", "PROJECT_ACTIVITY")
|
||||
.allSatisfy(
|
||||
type -> assertThat(type).isIn("CASE", "REFERENCE", "PROJECT_ACTIVITY", "RELEASE"));
|
||||
type ->
|
||||
assertThat(type)
|
||||
.isIn("CASE", "REFERENCE", "QUESTION", "PROJECT_ACTIVITY", "RELEASE"));
|
||||
assertThat(view.latestEntries())
|
||||
.as("게시한 Open Question 도 최근 기록에 나와야 한다 — 목록에서 빠지면 게시한 사실이 어디에도 보이지 않는다")
|
||||
.anySatisfy(
|
||||
entry -> {
|
||||
assertThat(entry.entryType()).isEqualTo("QUESTION");
|
||||
assertThat(entry.path()).isEqualTo("/questions/reprocessing-latency");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,10 +406,16 @@ class PublicSitePersistenceIntegrationTest {
|
||||
assertThat(view.relatedProjects()).extracting("title").contains("Tech Log");
|
||||
assertThat(view.latestRecords()).isNotEmpty();
|
||||
assertThat(view.latestRecords())
|
||||
.as("주제 상세의 최신 기록도 계약의 entryType 네 값을 벗어나면 안 된다")
|
||||
.as("주제 상세의 최신 기록도 계약의 entryType 을 벗어나면 안 된다")
|
||||
.extracting("entryType")
|
||||
.allSatisfy(
|
||||
type -> assertThat(type).isIn("CASE", "REFERENCE", "PROJECT_ACTIVITY", "RELEASE"));
|
||||
type ->
|
||||
assertThat(type)
|
||||
.isIn("CASE", "REFERENCE", "QUESTION", "PROJECT_ACTIVITY", "RELEASE"));
|
||||
assertThat(view.latestRecords())
|
||||
.as("주제와 홈은 같은 목록 의미를 쓴다 — 홈에 나오는 Open Question 이 여기서 빠지면 두 화면이 어긋난다")
|
||||
.extracting("entryType")
|
||||
.contains("QUESTION");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+8
@@ -9,17 +9,23 @@ import java.util.List;
|
||||
* <p>계약의 {@code CaseDetailResponse.case} 와 {@code ReferenceDetailResponse.reference} 는 담는 필드가
|
||||
* 다르지만(문제/결론 vs 범위/적용), 원천이 같은 {@code document} + 유형별 detail 이라 하나의 레코드로 읽고 웹 계층에서 유형별 모양으로 나눈다.
|
||||
*
|
||||
* @param summary 문서가 스스로 밝히는 한 줄 요약. 제목 바로 아래에 온다 — 유형별 요약({@code primarySummary}: Case 는 문제,
|
||||
* Reference 는 범위)과 다르다. 이 자리가 없던 동안 화면은 유형별 요약을 대신 썼고, 그러면 머리말이 바로 아래의 "문제" 나 "이 기준을 쓰는 이유" 와
|
||||
* 같은 글을 두 번 말했다.
|
||||
* @param content Markdown 원문이다. studio 의 렌더 블록이 아니다 — 공개 계약은 {@code contentFormat} 과 함께 원문을 준다.
|
||||
*/
|
||||
public record PublishedDocumentView(
|
||||
String type,
|
||||
String canonicalPath,
|
||||
String title,
|
||||
String summary,
|
||||
String primarySummary,
|
||||
String secondarySummary,
|
||||
List<String> environmentSummary,
|
||||
List<String> appliesTo,
|
||||
List<String> excludedScope,
|
||||
List<ReferenceRuleView> rules,
|
||||
List<String> examples,
|
||||
String freshnessStatus,
|
||||
String content,
|
||||
String contentFormat,
|
||||
@@ -37,6 +43,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);
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/**
|
||||
* Reference 의 판단 기준 한 줄. 계약 {@code ReferenceDetailResponse.reference.rules[]}.
|
||||
*
|
||||
* <p>Reference 의 본문은 마크다운 한 덩어리가 아니라 제목이 붙은 규칙의 목록이다 — Studio 의 편집기가 그렇게 받고, 공개 화면도 그렇게 그린다.
|
||||
*/
|
||||
public record ReferenceRuleView(String title, String body) {}
|
||||
@@ -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 @ 0ffbc28 (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 @ ef49d3a (master)
|
||||
7eb668e39e279e49767306dd36e1dd51302071c39d78495d21307bbd9676220e public-v1.yaml
|
||||
# source: tech-log-design-package contracts/openapi/studio-management-v1.yaml @ 0ffbc28 (master)
|
||||
72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55 studio-management-v1.yaml
|
||||
|
||||
@@ -1022,6 +1022,7 @@ components:
|
||||
enum:
|
||||
- CASE
|
||||
- REFERENCE
|
||||
- QUESTION
|
||||
- PROJECT_ACTIVITY
|
||||
- RELEASE
|
||||
title:
|
||||
@@ -1239,6 +1240,12 @@ components:
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
# 문서가 스스로 밝히는 한 줄 요약이다. 제목 바로 아래에 온다.
|
||||
#
|
||||
# 이 자리가 없어서 화면은 problemSummary / scopeSummary 를 대신 썼고, 그러면 머리말이
|
||||
# 바로 아래의 "문제" 나 "이 기준을 쓰는 이유" 와 같은 글을 두 번 말한다.
|
||||
summary:
|
||||
type: string
|
||||
problemSummary:
|
||||
type: string
|
||||
conclusionSummary:
|
||||
@@ -1325,6 +1332,12 @@ components:
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
# 문서가 스스로 밝히는 한 줄 요약이다. 제목 바로 아래에 온다.
|
||||
#
|
||||
# 이 자리가 없어서 화면은 problemSummary / scopeSummary 를 대신 썼고, 그러면 머리말이
|
||||
# 바로 아래의 "문제" 나 "이 기준을 쓰는 이유" 와 같은 글을 두 번 말한다.
|
||||
summary:
|
||||
type: string
|
||||
scopeSummary:
|
||||
type: string
|
||||
appliesTo:
|
||||
@@ -1335,6 +1348,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:
|
||||
|
||||
Reference in New Issue
Block a user