From a7e2b7d7feb7bc21c8b03e7a1f6145cf5438b22b Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Mon, 24 Aug 2026 15:45:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B0=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=ED=99=9C=EB=8F=99=EC=9D=84=20?= =?UTF-8?q?=EB=82=A8=EA=B8=B0=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 활동은 손으로 적는 자리였다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린 사실과 따로 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다. 게시가 곧 사건이므로 게시가 기록한다 (publish 19단계). 문서마다 한 줄만 남긴다. 재게시는 새로 올린 것이 아니라 같은 글을 고친 것이므로 타임라인에 다시 나타나지 않아야 한다 — `operation_key` 를 `publication:` 로 두고 `uq_project_activity_operation_key` 충돌을 무시한다. `origin` 은 `AUTO` 다. 손으로 적은 줄과 구분해 두면, 삭제 금지 규칙이 실제 사건의 흔적만 지킨다. V10 은 이 규칙을 이미 게시된 것들에 소급 적용한다. 게시는 있었는데 로그가 없는 상태를 남겨 두면 이 변경 이전에 올린 글은 타임라인에서 영영 빠진다. `occurred_at` 은 최초 PUBLISHED 사건의 시각이다 — now() 를 쓰면 옛 게시가 전부 오늘 올린 것처럼 보인다. 함께 고친 것: 마이그레이션 버전 목록이 7 에서 멈춰 있었다. TechLog 가 들어오며 8·9 가 붙었는데 이 테스트를 같이 고치지 않아, 그 빨간색은 "스키마가 잘못됐다" 가 아니라 "목록을 안 고쳤다" 를 뜻하는 상태로 두 번 지나갔다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu --- .../JdbcPublicationWriterAdapter.java | 47 ++++++++++++++++ ...chlog_project_activity_publication_log.sql | 53 +++++++++++++++++++ .../PostgreSqlMigrationIntegrationTest.java | 10 +++- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__techlog_project_activity_publication_log.sql diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/studio/publication/JdbcPublicationWriterAdapter.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/studio/publication/JdbcPublicationWriterAdapter.java index 6d7f64f..753f266 100644 --- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/studio/publication/JdbcPublicationWriterAdapter.java +++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/techlog/studio/publication/JdbcPublicationWriterAdapter.java @@ -164,9 +164,56 @@ public class JdbcPublicationWriterAdapter implements PublicationWriterPort { // 18. Document publish metadata markSourcePublished(request); + // 19. 프로젝트 활동 로그 + recordProjectActivity(request); + return result(publicationId, eventId); } + /** + * 프로젝트 활동은 손으로 적는 것이 아니라 게시가 남기는 로그다. + * + *

한동안 이 줄을 Studio 에서 직접 써야 했다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린 사실과 따로 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다. + * 게시가 곧 사건이므로 게시가 기록한다. + * + *

{@code operation_key} 로 문서마다 한 줄만 남긴다. 재게시는 새로 올린 것이 아니라 같은 글을 고친 것이므로 타임라인에 다시 나타나지 않아야 한다 + * — {@code uq_project_activity_operation_key} 가 그것을 보장하고, 여기서는 충돌을 무시한다. + * + *

프로젝트에 매달리지 않은 기록은 남길 자리가 없다. 그때는 아무것도 하지 않는다. + */ + private void recordProjectActivity(PublishRequest request) { + if (request.projectId() == null) { + return; + } + jdbcClient + .sql( + "INSERT INTO project_activity (id, project_id, activity_type, title, summary," + + " visibility, origin, related_resource_type, related_resource_id, occurred_at," + + " operation_key, created_by, updated_by)" + + " VALUES (:id, :projectId, :type, :title, '', 'PUBLIC', 'AUTO'," + + " :resourceType, :resourceId, now(), :operationKey, :actor, :actor)" + + " ON CONFLICT (project_id, operation_key) DO NOTHING") + .param("id", idGenerator.get()) + .param("projectId", request.projectId()) + .param("type", activityTypeOf(request.kind())) + .param("title", request.title()) + .param("resourceType", request.kind().name()) + .param("resourceId", request.documentId()) + .param("operationKey", "publication:" + request.documentId()) + .param("actor", request.principal()) + .update(); + } + + /** {@code project_activity_activity_type_check} 가 허용하는 값으로 옮긴다. */ + private static String activityTypeOf(RecordKind kind) { + return switch (kind) { + case CASE -> "CASE_PUBLISHED"; + case REFERENCE -> "REFERENCE_PUBLISHED"; + case QUESTION -> "QUESTION_OPENED"; + case PROJECT_DECISION -> "DECISION_ACCEPTED"; + }; + } + @Override public PublishResultView unpublish(UnpublishRequest request) { requireTransaction("unpublish"); diff --git a/src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__techlog_project_activity_publication_log.sql b/src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__techlog_project_activity_publication_log.sql new file mode 100644 index 0000000..5e2809b --- /dev/null +++ b/src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__techlog_project_activity_publication_log.sql @@ -0,0 +1,53 @@ +-- 프로젝트 활동을 게시 로그로 되돌린다. +-- +-- 이 표는 원래 손으로 적는 자리였다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린 사실과 따로 +-- 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다. 이제 게시가 이 줄을 남긴다 +-- (JdbcPublicationWriterAdapter 19단계). +-- +-- 이 마이그레이션은 그 규칙을 이미 게시된 것들에 소급 적용한다. 게시는 있었는데 로그가 없는 +-- 상태를 남겨 두면, 이 변경 이전에 올린 글은 타임라인에서 영영 빠진다. +-- +-- occurred_at 은 최초 PUBLISHED 사건의 시각이다 -- now() 를 쓰면 옛 게시가 전부 오늘 올린 것처럼 +-- 보인다. operation_key 는 애플리케이션이 쓰는 것과 같은 규칙이라, 나중에 같은 문서를 재게시해도 +-- 줄이 늘지 않는다. +INSERT INTO project_activity ( + id, project_id, activity_type, title, summary, visibility, origin, + related_resource_type, related_resource_id, occurred_at, + operation_key, created_by, updated_by +) +SELECT + gen_random_uuid(), + link.project_id, + CASE publication.source_kind + WHEN 'CASE' THEN 'CASE_PUBLISHED' + WHEN 'REFERENCE' THEN 'REFERENCE_PUBLISHED' + WHEN 'QUESTION' THEN 'QUESTION_OPENED' + ELSE 'DECISION_ACCEPTED' + END, + projection.title, + '', + 'PUBLIC', + 'AUTO', + publication.source_kind, + publication.source_id, + first_published.occurred_at, + 'publication:' || publication.source_id, + 'system:migration', + 'system:migration' +FROM publication +JOIN public_resource_project_link link + ON link.resource_type = publication.source_kind + AND link.resource_id = publication.source_id + AND link.relation_type = 'PRIMARY' +JOIN public_resource_projection projection + ON projection.resource_type = publication.source_kind + AND projection.resource_id = publication.source_id +JOIN LATERAL ( + SELECT min(event.occurred_at) AS occurred_at + FROM publication_event event + WHERE event.publication_id = publication.publication_id + AND event.event_type = 'PUBLISHED' +) first_published ON true +WHERE publication.status = 'PUBLISHED' + AND first_published.occurred_at IS NOT NULL +ON CONFLICT (project_id, operation_key) DO NOTHING; diff --git a/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlMigrationIntegrationTest.java b/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlMigrationIntegrationTest.java index 96451ce..640a985 100644 --- a/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlMigrationIntegrationTest.java +++ b/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlMigrationIntegrationTest.java @@ -43,8 +43,16 @@ class PostgreSqlMigrationIntegrationTest { .load() .migrate(); + /* + TechLog 가 들어오면서 8·9·10 이 붙었는데 이 목록은 7 에서 멈춰 있었다. 마이그레이션을 더한 + 사람이 여기를 같이 고치지 않으면 이 테스트만 빨개지고, 그 빨간색은 "스키마가 잘못됐다" 가 + 아니라 "목록을 안 고쳤다" 를 뜻한다 — 정확히 그 상태로 두 번 지나갔다. + + 목록을 고정해 두는 이유는 남아 있다: 마이그레이션이 순서대로, 빠짐없이 적용되는지 확인한다. + 그래서 개수를 세는 것이 아니라 버전을 그대로 적는다. + */ assertThat(appliedVersions(postgres, "flyway_schema_history")) - .containsExactly("1", "3", "4", "5", "6", "7"); + .containsExactly("1", "3", "4", "5", "6", "7", "8", "9", "10"); Flyway coreStream = Flyway.configure()