feat: 게시가 프로젝트 활동을 남기게 한다

프로젝트 활동은 손으로 적는 자리였다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린
사실과 따로 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다. 게시가 곧 사건이므로
게시가 기록한다 (publish 19단계).

문서마다 한 줄만 남긴다. 재게시는 새로 올린 것이 아니라 같은 글을 고친 것이므로
타임라인에 다시 나타나지 않아야 한다 — `operation_key` 를
`publication:<documentId>` 로 두고 `uq_project_activity_operation_key` 충돌을 무시한다.

`origin` 은 `AUTO` 다. 손으로 적은 줄과 구분해 두면, 삭제 금지 규칙이 실제 사건의
흔적만 지킨다.

V10 은 이 규칙을 이미 게시된 것들에 소급 적용한다. 게시는 있었는데 로그가 없는 상태를
남겨 두면 이 변경 이전에 올린 글은 타임라인에서 영영 빠진다. `occurred_at` 은 최초
PUBLISHED 사건의 시각이다 — now() 를 쓰면 옛 게시가 전부 오늘 올린 것처럼 보인다.

함께 고친 것: 마이그레이션 버전 목록이 7 에서 멈춰 있었다. TechLog 가 들어오며 8·9 가
붙었는데 이 테스트를 같이 고치지 않아, 그 빨간색은 "스키마가 잘못됐다" 가 아니라
"목록을 안 고쳤다" 를 뜻하는 상태로 두 번 지나갔다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
DongHyeonka
2026-08-24 15:45:36 +09:00
co-authored by Claude Opus 5
parent 6aa140077d
commit a7e2b7d7fe
3 changed files with 109 additions and 1 deletions
@@ -164,9 +164,56 @@ public class JdbcPublicationWriterAdapter implements PublicationWriterPort {
// 18. Document publish metadata // 18. Document publish metadata
markSourcePublished(request); markSourcePublished(request);
// 19. 프로젝트 활동 로그
recordProjectActivity(request);
return result(publicationId, eventId); return result(publicationId, eventId);
} }
/**
* 프로젝트 활동은 손으로 적는 것이 아니라 게시가 남기는 로그다.
*
* <p>한동안 이 줄을 Studio 에서 직접 써야 했다. 그러면 "언제 무엇을 올렸는가" 가 실제로 올린 사실과 따로 관리되고, 적기를 잊으면 타임라인에 구멍이 남는다.
* 게시가 곧 사건이므로 게시가 기록한다.
*
* <p>{@code operation_key} 로 문서마다 한 줄만 남긴다. 재게시는 새로 올린 것이 아니라 같은 글을 고친 것이므로 타임라인에 다시 나타나지 않아야 한다
* — {@code uq_project_activity_operation_key} 가 그것을 보장하고, 여기서는 충돌을 무시한다.
*
* <p>프로젝트에 매달리지 않은 기록은 남길 자리가 없다. 그때는 아무것도 하지 않는다.
*/
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 @Override
public PublishResultView unpublish(UnpublishRequest request) { public PublishResultView unpublish(UnpublishRequest request) {
requireTransaction("unpublish"); requireTransaction("unpublish");
@@ -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;
@@ -43,8 +43,16 @@ class PostgreSqlMigrationIntegrationTest {
.load() .load()
.migrate(); .migrate();
/*
TechLog 가 들어오면서 8·9·10 이 붙었는데 이 목록은 7 에서 멈춰 있었다. 마이그레이션을 더한
사람이 여기를 같이 고치지 않으면 이 테스트만 빨개지고, 그 빨간색은 "스키마가 잘못됐다" 가
아니라 "목록을 안 고쳤다" 를 뜻한다 — 정확히 그 상태로 두 번 지나갔다.
목록을 고정해 두는 이유는 남아 있다: 마이그레이션이 순서대로, 빠짐없이 적용되는지 확인한다.
그래서 개수를 세는 것이 아니라 버전을 그대로 적는다.
*/
assertThat(appliedVersions(postgres, "flyway_schema_history")) 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 coreStream =
Flyway.configure() Flyway.configure()