From edb0890dc8236894a98eddba012e66e6472b4641 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Wed, 26 Aug 2026 23:43:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=99=88=EA=B3=BC=20=EC=A3=BC=EC=A0=9C?= =?UTF-8?q?=EC=9D=98=20=EC=B5=9C=EA=B7=BC=20=EA=B8=B0=EB=A1=9D=EC=9D=B4=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=90=9C=20Open=20Question=20=EC=9D=84=20?= =?UTF-8?q?=EB=8B=B4=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 질문을 게시해도 홈 최근 기록에 나오지 않았다. 백엔드가 담지 않은 것이 아니라 담을 수 없었다 — 계약의 `LatestEntry.entryType` 이 CASE/REFERENCE/PROJECT_ACTIVITY/RELEASE 넷만 허용했고, 계약 밖 값을 응답 매퍼에 넘기면 500 이 되어 홈 화면 전체를 못 쓰게 만들기 때문에 질의가 먼저 걸러 내고 있었다. 계약을 넓혔으므로(design-package ef49d3a) 걸러 낼 이유가 사라졌다. `LATEST_ENTRY_TYPES` 에 `QUESTION` 을 더한다. `pathOf` 는 이미 `/questions/{slug}` 를 만들고 있었고, projection 에도 질문 행이 `ACTIVE`/`PUBLIC` 으로 `navigation_path` 까지 채워진 채 들어 있었다 — 막고 있던 것은 이 `IN` 목록 하나였다. `LATEST_ENTRY_TYPES` 는 홈과 주제 상세가 함께 쓴다. 두 목록의 의미가 같으므로 한 곳만 넓히면 둘 다 따라오고, 그것이 의도다. 두 화면 각각에 "게시한 Open Question 이 목록에 나온다"는 단언을 세워 둔다 — 유형 허용 목록만 검사하면 담기지 않아도 통과한다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0189NzCryfeqDzS81EWidnBx --- .../JdbcPublicSiteQueryAdapter.java | 4 +-- .../techlog/publicsite/PublicSql.java | 2 +- .../PublicSitePersistenceIntegrationTest.java | 25 +++++++++++++++---- src/config/openapi/MANIFEST.sha256 | 4 +-- src/config/openapi/public-v1.yaml | 1 + 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicSiteQueryAdapter.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicSiteQueryAdapter.java index 6648395..db3a237 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicSiteQueryAdapter.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/JdbcPublicSiteQueryAdapter.java @@ -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 은 홈 화면 전체를 못 쓰게 만든다. * *

{@code RELEASE} 가 결과에 없는 것은 누락이 아니다. 릴리스는 Publication 파이프라인을 거치지 않고 자체 {@code diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSql.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSql.java index 4d9487c..58e1fdb 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSql.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSql.java @@ -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() {} diff --git a/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSitePersistenceIntegrationTest.java b/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSitePersistenceIntegrationTest.java index e674b84..244e69d 100644 --- a/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSitePersistenceIntegrationTest.java +++ b/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/techlog/publicsite/PublicSitePersistenceIntegrationTest.java @@ -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 diff --git a/src/config/openapi/MANIFEST.sha256 b/src/config/openapi/MANIFEST.sha256 index 2e2ceb4..3320ae9 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 @ 0ffbc28 (master) 18dd46898be64b07f7e826409d19347512613ee2e22420028a4a0644f50f37dd studio-v1.yaml -# source: tech-log-design-package contracts/openapi/public-v1.yaml @ 0ffbc28 (master) -79949b286e2b4d28333d3376945960a319d247b675b23a73f50b75ce10994c5e public-v1.yaml +# 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 diff --git a/src/config/openapi/public-v1.yaml b/src/config/openapi/public-v1.yaml index 28dd4f5..1631d17 100644 --- a/src/config/openapi/public-v1.yaml +++ b/src/config/openapi/public-v1.yaml @@ -1022,6 +1022,7 @@ components: enum: - CASE - REFERENCE + - QUESTION - PROJECT_ACTIVITY - RELEASE title: