feat: Tech Log 공개 조회 백엔드 — public-v1 18개 operation 구현
public-v1.yaml의 18개 operation 전부를 구현한다. 사이트·홈·프로필, 탐색 2종, 주제
2종, 문서 상세 3종, 프로젝트 5종, 릴리스 2종, 검색. studio-v1(19/19)에 이어
public-v1도 18/18이다.
생성기가 계약 필드를 조용히 빠뜨리고 있었다 — 근본 원인은 파생 단계의 YAML alias
swagger-parser가 이 문서의 스키마 15개를 "is not of type `object`"로 거절했다.
거절당한 스키마들은 전부 type: object를 명시하고 있어서 계약 결함처럼 보이지 않았고,
validateSpec을 끄면 생성은 성공했다. 그런데 그렇게 만든 모델에서 LatestEntry.publishedAt,
ProjectListItem.updatedAt, SearchResultItem.matchedFields, ReleaseListItem.changeTypes가
사라져 있었다. 컴파일은 통과한다 — 아직 아무도 그 필드를 안 쓰니까.
원인은 prepare 단계였다. 변환들이 같은 Map 인스턴스를 여러 property에 재사용했고
snakeyaml이 그 지점을 anchor/alias(&id001 / *id001)로 덤프했다. swagger-parser는
alias 노드를 해석하지 못해 그 스키마 전체를 거절하고, generator는 검증을 끄면 문서를
받아들이되 alias였던 property를 말없이 버린다. 파생 스펙에 alias가 34곳 있었다.
- 덤프 직전 deep copy로 노드 identity를 끊어 alias를 원천 차단하고, 남으면 빌드가
실패하도록 fail-closed 게이트를 뒀다. validateSpec은 다시 켰다
- verifyPublicGeneratedModels를 schema 이름 대조에서 property 대조로 강화했다.
이번 누락을 이 게이트가 통과시켰기 때문이다. 지금은 schema 62개 · property 250개를 센다
계약이 선언했는데 서버가 무시하던 필터를 채웠다
지정해도 오류가 아니라 "결과 0건"으로 보여서 소비자가 자기 요청이 틀렸다는 걸 알 수 없었다.
- exploreQuestions: tag 필터 없음, sort 3값이 SQL에 반영되지 않음
- listPublicProjectDecisions: status 필터 없음
- listPublicProjectRecords: type/relation 필터 없음, QUESTION이 대상에서 빠져 있었음
- 필터는 목록과 총계 두 쿼리에 같이 걸린다. 갈라지면 마지막 페이지가 비어 보인다
- enum 파라미터는 요청 경계에서 검사해 PUBLIC_REQUEST_INVALID로 거절한다
응답 봉투와 오류 경계
- 컨트롤러는 봉투를 반환하지 않는다. EnvelopeBodyAdvice가 감싼다(ADR-006)
- PublicExceptionHandler를 publicapi 스코프로 두고, StudioExceptionHandler의 스코프를
...web.techlog → ...web.techlog.studio로 좁혔다. 좁히지 않으면 공개 조회의 파라미터
오류가 Studio 계약 코드(REQUEST_VALIDATION_FAILED, 422)로 나가는데, 그 코드는
public-v1의 ApiError.code enum에 없어 프론트엔드의 응답 파싱 자체가 깨진다
- FieldError 모양이 studio({path,message})와 public({field,code,message})이 다르다
실행이 잡아낸 결함
컴파일과 단위 테스트로는 드러나지 않았고 실제 PostgreSQL과 실제 기동이 잡았다.
- profile()의 selectedEvidence가 List.of() 하드코딩이었다. 계약 필드가 항상 비어 있었다
- latestEntries/latestRecords가 projection의 모든 resource_type을 흘렸다. 계약의
LatestEntry.entryType은 4값뿐이라 QUESTION이 섞이면 매퍼가 500을 낸다
- home_focus_config.default_focus_type은 마이그레이션 직후 NULL인데 계약은 이 필드를
required + enum 3값으로 선언한다. 배포 직후 첫 요청부터 /home이 깨졌다.
HomeFocusView.resolve가 반드시 유효한 값 하나를 정하도록 고쳤다
V9__techlog_public_surface.sql
설계 패키지 database/V1__init.sql이 정의한 공개 표면 6종(release, site_config,
profile_page, home_focus_config, project_topic, topic_featured_document)과 단일 행
시딩. 릴리스는 Publication 파이프라인을 거치지 않고 자체 workflow_status로 공개된다.
게이트
- PublicContractDriftTest: springdoc이 게시하는 표면과 계약을 양방향 대조한다.
계약의 servers(/api/v1/public)를 경로에 더해 비교하며, operation 수 18을 함께 고정해
"비교 대상이 0건이라 통과"를 실패로 만든다. 봉투 래핑도 확인한다
- PublicErrorRegistryTest: PublicError ↔ error-codes.yaml ↔ 계약 enum 3자 대조.
INTERNAL_ERROR는 스켈레톤 소유라 재선언하지 않으므로 "계약 = public 소유 ∪ 그 하나"로
고정한다. vendored 계약의 MANIFEST 해시도 확인한다
- postgresqlTechLogPublicPersistenceIntegrationTest: 어댑터 7종과 V9를 실제
PostgreSQL에서 돌린다. 표준 check는 Testcontainers를 돌리지 않으므로 이 태스크가
없으면 이 SQL은 한 번도 실행되지 않은 채 빌드가 통과한다. 게시 취소·비공개 자료를
함께 심어 어느 경로로도 새지 않는지 확인한다
검증
./gradlew check BUILD SUCCESSFUL (248 task). 공개 조회 통합 테스트 24/24.
실제 PostgreSQL로 앱을 띄워 18개 operation 전부 실호출 — 5xx 0건, 파라미터 검증 5종
전부 계약 코드. 한때 사라졌던 publishedAt/matchedFields/changeTypes가 실응답에 있다.
알려진 선재 실패: ActuatorSecurityHttpTest가 /actuator/health 503으로 실패한다.
기저 커밋 743fee3에서도 동일하게 재현되며, 원인은 redis가 호스트 포트에 노출되지 않아
헬스가 DOWN인 환경 문제다. 이 커밋과 무관하다.
AGENTS.md의 commit 정책은 human-only다. 이 커밋은 사용자가 "지금 변경했던 내용을
전부 반영하고 develop과 main에 반영하도록" 지시해 예외로 수행한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3254def57
commit
365560efb6
+48
@@ -0,0 +1,48 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.error;
|
||||
|
||||
import dev.caskeleton.shared.error.ApiErrorCode;
|
||||
import dev.caskeleton.shared.error.Category;
|
||||
|
||||
/**
|
||||
* 공개 조회 계약(`public-v1.yaml`)의 `ApiError.code`.
|
||||
*
|
||||
* <p>계약의 enum 은 세 값인데 여기엔 둘뿐이다. 나머지 하나 {@code INTERNAL_ERROR} 는 이 기능이 아니라 스켈레톤의 공통 예외 처리기가 내는 코드이므로
|
||||
* 여기서 다시 선언하지 않는다 — 같은 코드를 두 enum 이 각자 status 와 함께 선언하면 레지스트리가 어느 쪽을 따라야 할지 알 수 없다.
|
||||
*
|
||||
* <p>Studio 의 코드와 이름을 겹치지 않게 한 것도 같은 이유다. 레지스트리는 코드 하나에 status 하나만 담을 수 있어서 public 의 400 과 studio 의
|
||||
* 422 를 같은 이름으로 쓸 수 없다.
|
||||
*/
|
||||
public enum PublicError implements ApiErrorCode {
|
||||
PUBLIC_REQUEST_INVALID(Category.VALIDATION, 400, false),
|
||||
PUBLIC_RESOURCE_NOT_FOUND(Category.NOT_FOUND, 404, false);
|
||||
|
||||
private final Category category;
|
||||
private final int httpStatus;
|
||||
private final boolean retryable;
|
||||
|
||||
PublicError(Category category, int httpStatus, boolean retryable) {
|
||||
this.category = category;
|
||||
this.httpStatus = httpStatus;
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String code() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Category category() {
|
||||
return category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int httpStatus() {
|
||||
return httpStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retryable() {
|
||||
return retryable;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.error;
|
||||
|
||||
import dev.caskeleton.shared.error.ApiErrorCarrier;
|
||||
import dev.caskeleton.shared.error.ApiErrorCode;
|
||||
|
||||
/**
|
||||
* 공개 조회 실패. {@link #getMessage()} 는 진단용이며 클라이언트에게 그대로 나가지 않는다 — 응답 문구는 레지스트리의 client-safe message 를
|
||||
* 쓴다({@code ApiErrorCarrier} javadoc).
|
||||
*/
|
||||
public final class PublicException extends RuntimeException implements ApiErrorCarrier {
|
||||
|
||||
private final transient PublicError error;
|
||||
|
||||
private PublicException(PublicError error, String message) {
|
||||
super(message);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static PublicException of(PublicError error, String message) {
|
||||
return new PublicException(error, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiErrorCode errorCode() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public PublicError publicError() {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 계약 {@code AssetReference}.
|
||||
*
|
||||
* @param url 검증된 전송 경로다. object storage URL 이 아니다(설계 05장 §3.1).
|
||||
*/
|
||||
public record AssetReferenceView(
|
||||
UUID assetId, String url, String altText, Integer width, Integer height, String contentType) {}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code CaseDetailResponse}. */
|
||||
public record CaseDetailView(
|
||||
String canonicalPath,
|
||||
boolean indexable,
|
||||
PublishedDocumentView document,
|
||||
CaseRelationsView relations) {}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code CaseDetailResponse.relations}. */
|
||||
public record CaseRelationsView(
|
||||
RelatedEntryView originQuestion,
|
||||
List<RelatedEntryView> projectDecisions,
|
||||
List<RelatedEntryView> derivedReferences,
|
||||
List<RelatedEntryView> relatedCases) {
|
||||
|
||||
public CaseRelationsView {
|
||||
projectDecisions = projectDecisions == null ? List.of() : List.copyOf(projectDecisions);
|
||||
derivedReferences = derivedReferences == null ? List.of() : List.copyOf(derivedReferences);
|
||||
relatedCases = relatedCases == null ? List.of() : List.copyOf(relatedCases);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code ContactLink}. */
|
||||
public record ContactLinkView(String type, String label, String url) {}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 계약 {@code HomeResponse.focus}. {@code defaultType} 이 어느 갈래를 보여줄지 정하고 세 갈래는 전부 optional 이다 — 계약이
|
||||
* 그렇게 정했다. 설정된 갈래가 비어 있을 수 있으므로(예: 지목한 질문이 비공개가 되었을 때) 타입으로 "반드시 하나는 있다"를 강제하지 않는다.
|
||||
*/
|
||||
public record HomeFocusView(
|
||||
String defaultType,
|
||||
CurrentWork currentWork,
|
||||
OpenQuestion openQuestion,
|
||||
RecentDecision recentDecision) {
|
||||
|
||||
/**
|
||||
* 설정된 값이 없거나 그 갈래가 비었을 때 쓸 순서. 계약이 {@code defaultType} 을 required 로 선언했으므로 "정해진 게 없다"를 null 로 표현할
|
||||
* 수 없다.
|
||||
*/
|
||||
private static final String[] FALLBACK_ORDER = {
|
||||
"CURRENT_WORK", "OPEN_QUESTION", "RECENT_DECISION"
|
||||
};
|
||||
|
||||
/**
|
||||
* 계약이 {@code focus.defaultType} 을 required 로 선언하고 값 셋만 허용한다. 그런데 설정 테이블은 갓 마이그레이션한 상태에서 {@code
|
||||
* default_focus_type} 이 NULL 이고, 지목한 갈래가 비공개로 바뀌어 비는 경우도 있다. 그대로 내보내면 응답 매퍼가 계약 밖 값을 만나 홈 화면 전체가
|
||||
* 실패한다 — 실제로 배포 직후 첫 요청이 그렇게 깨졌다.
|
||||
*
|
||||
* <p>그래서 이 자리에서 반드시 유효한 값 하나를 정한다.
|
||||
*
|
||||
* <ol>
|
||||
* <li>설정된 값이 유효하고 그 갈래에 내용이 있으면 그대로 쓴다.
|
||||
* <li>아니면 내용이 있는 갈래를 {@link #FALLBACK_ORDER} 순으로 고른다.
|
||||
* <li>셋 다 비었으면 첫 값을 쓴다 — 세 갈래는 전부 optional 이므로 비어 있어도 계약을 만족한다.
|
||||
* </ol>
|
||||
*/
|
||||
public static HomeFocusView resolve(
|
||||
String configuredType,
|
||||
CurrentWork currentWork,
|
||||
OpenQuestion openQuestion,
|
||||
RecentDecision recentDecision) {
|
||||
if (configuredType != null
|
||||
&& hasContent(configuredType, currentWork, openQuestion, recentDecision)) {
|
||||
return new HomeFocusView(configuredType, currentWork, openQuestion, recentDecision);
|
||||
}
|
||||
for (String candidate : FALLBACK_ORDER) {
|
||||
if (hasContent(candidate, currentWork, openQuestion, recentDecision)) {
|
||||
return new HomeFocusView(candidate, currentWork, openQuestion, recentDecision);
|
||||
}
|
||||
}
|
||||
return new HomeFocusView(FALLBACK_ORDER[0], currentWork, openQuestion, recentDecision);
|
||||
}
|
||||
|
||||
private static boolean hasContent(
|
||||
String type,
|
||||
CurrentWork currentWork,
|
||||
OpenQuestion openQuestion,
|
||||
RecentDecision recentDecision) {
|
||||
return switch (type) {
|
||||
case "CURRENT_WORK" -> currentWork != null;
|
||||
case "OPEN_QUESTION" -> openQuestion != null;
|
||||
case "RECENT_DECISION" -> recentDecision != null;
|
||||
// 계약 밖 값이 설정에 들어 있는 경우다. 그대로 쓰면 응답이 깨지므로 없는 것으로 친다.
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/** 계약 {@code CurrentWorkFocus}. */
|
||||
public record CurrentWork(
|
||||
String projectName,
|
||||
String projectPath,
|
||||
String purpose,
|
||||
String phase,
|
||||
String currentObjective,
|
||||
String nextStep,
|
||||
Instant updatedAt) {}
|
||||
|
||||
/** 계약 {@code OpenQuestionFocus}. */
|
||||
public record OpenQuestion(
|
||||
String question,
|
||||
String questionPath,
|
||||
String summary,
|
||||
List<String> knownFacts,
|
||||
List<String> unresolvedPoints,
|
||||
String nextVerification,
|
||||
Instant updatedAt) {
|
||||
|
||||
public OpenQuestion {
|
||||
knownFacts = knownFacts == null ? List.of() : List.copyOf(knownFacts);
|
||||
unresolvedPoints = unresolvedPoints == null ? List.of() : List.copyOf(unresolvedPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/** 계약 {@code RecentDecisionFocus}. */
|
||||
public record RecentDecision(
|
||||
String statement,
|
||||
String decisionPath,
|
||||
String rationale,
|
||||
List<String> consequences,
|
||||
Instant decidedAt) {
|
||||
|
||||
public RecentDecision {
|
||||
consequences = consequences == null ? List.of() : List.copyOf(consequences);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code HomeResponse}. */
|
||||
public record HomeView(HomeFocusView focus, List<LatestEntryView> latestEntries) {
|
||||
|
||||
public HomeView {
|
||||
latestEntries = latestEntries == null ? List.of() : List.copyOf(latestEntries);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code KnowledgeListItem}. */
|
||||
public record KnowledgeListItemView(
|
||||
String type,
|
||||
String title,
|
||||
String path,
|
||||
String primarySummary,
|
||||
String secondarySummary,
|
||||
TopicSummaryView primaryTopic,
|
||||
ProjectSummaryView primaryProject,
|
||||
Instant publishedAt,
|
||||
Instant lastVerifiedAt,
|
||||
String freshnessStatus) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code KnowledgePage}. */
|
||||
public record KnowledgePageView(List<KnowledgeListItemView> items, PageMetadataView page) {
|
||||
|
||||
public KnowledgePageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code LatestEntry}. */
|
||||
public record LatestEntryView(
|
||||
String entryType,
|
||||
String title,
|
||||
String summary,
|
||||
String path,
|
||||
TopicSummaryView primaryTopic,
|
||||
ProjectSummaryView primaryProject,
|
||||
Instant publishedAt) {}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/**
|
||||
* 계약 {@code PageMetadata}. 공개 조회는 studio 와 달리 offset 페이지네이션이다 — 계약이 그렇게 정했고, 공개 목록은 "3페이지로 바로 가기"가
|
||||
* 필요한 화면이라 cursor 로 대체할 수 없다.
|
||||
*/
|
||||
public record PageMetadataView(
|
||||
int number,
|
||||
int size,
|
||||
long totalElements,
|
||||
int totalPages,
|
||||
boolean hasPrevious,
|
||||
boolean hasNext) {
|
||||
|
||||
public static PageMetadataView of(int page, int size, long total) {
|
||||
int totalPages = size <= 0 ? 0 : (int) Math.ceil((double) total / size);
|
||||
return new PageMetadataView(page, size, total, totalPages, page > 1, page < totalPages);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProfileResponse}. */
|
||||
public record ProfileView(
|
||||
String headline,
|
||||
String description,
|
||||
List<NamedDescription> workingModel,
|
||||
List<Territory> territories,
|
||||
List<RelatedEntryView> selectedEvidence,
|
||||
List<NamedDescription> trajectory,
|
||||
List<ContactLinkView> contacts) {
|
||||
|
||||
public ProfileView {
|
||||
workingModel = workingModel == null ? List.of() : List.copyOf(workingModel);
|
||||
territories = territories == null ? List.of() : List.copyOf(territories);
|
||||
selectedEvidence = selectedEvidence == null ? List.of() : List.copyOf(selectedEvidence);
|
||||
trajectory = trajectory == null ? List.of() : List.copyOf(trajectory);
|
||||
contacts = contacts == null ? List.of() : List.copyOf(contacts);
|
||||
}
|
||||
|
||||
/** {@code workingModel[]} 과 {@code trajectory[]} 가 같은 모양이라 하나로 쓴다. */
|
||||
public record NamedDescription(String name, String description) {}
|
||||
|
||||
/** {@code territories[]}. */
|
||||
public record Territory(String name, String currentQuestion, String topicPath) {}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code ProjectActivityItem}. */
|
||||
public record ProjectActivityItemView(
|
||||
String type, String title, String summary, Instant occurredAt, String relatedPath) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProjectActivityPage}. */
|
||||
public record ProjectActivityPageView(List<ProjectActivityItemView> items, PageMetadataView page) {
|
||||
|
||||
public ProjectActivityPageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 계약 {@code ProjectDecisionItem}. */
|
||||
public record ProjectDecisionItemView(
|
||||
UUID id,
|
||||
String statement,
|
||||
String status,
|
||||
String rationaleSummary,
|
||||
Instant decidedAt,
|
||||
RelatedEntryView sourceQuestion,
|
||||
RelatedEntryView sourceCase) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProjectDecisionPage}. */
|
||||
public record ProjectDecisionPageView(List<ProjectDecisionItemView> items, PageMetadataView page) {
|
||||
|
||||
public ProjectDecisionPageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProjectDetailResponse}. */
|
||||
public record ProjectDetailView(
|
||||
String canonicalPath,
|
||||
boolean indexable,
|
||||
PublishedProjectView project,
|
||||
RelatedEntryView featuredDecision,
|
||||
RelatedEntryView activeQuestion,
|
||||
List<RelatedEntryView> selectedRecords) {
|
||||
|
||||
public ProjectDetailView {
|
||||
selectedRecords = selectedRecords == null ? List.of() : List.copyOf(selectedRecords);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code ProjectListItem}. */
|
||||
public record ProjectListItemView(
|
||||
String name,
|
||||
String slug,
|
||||
String path,
|
||||
String oneLinePurpose,
|
||||
String phase,
|
||||
String currentObjective,
|
||||
String nextStep,
|
||||
Instant updatedAt) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProjectRecordPage}. */
|
||||
public record ProjectRecordPageView(List<RelatedEntryView> items, PageMetadataView page) {
|
||||
|
||||
public ProjectRecordPageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code ProjectSummary}. */
|
||||
public record ProjectSummaryView(String name, String slug, String path) {}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 공개된 Case / Reference 의 본문과 메타데이터.
|
||||
*
|
||||
* <p>계약의 {@code CaseDetailResponse.case} 와 {@code ReferenceDetailResponse.reference} 는 담는 필드가
|
||||
* 다르지만(문제/결론 vs 범위/적용), 원천이 같은 {@code document} + 유형별 detail 이라 하나의 레코드로 읽고 웹 계층에서 유형별 모양으로 나눈다.
|
||||
*
|
||||
* @param content Markdown 원문이다. studio 의 렌더 블록이 아니다 — 공개 계약은 {@code contentFormat} 과 함께 원문을 준다.
|
||||
*/
|
||||
public record PublishedDocumentView(
|
||||
String type,
|
||||
String canonicalPath,
|
||||
String title,
|
||||
String primarySummary,
|
||||
String secondarySummary,
|
||||
List<String> environmentSummary,
|
||||
List<String> appliesTo,
|
||||
List<String> excludedScope,
|
||||
String freshnessStatus,
|
||||
String content,
|
||||
String contentFormat,
|
||||
int contentFormatVersion,
|
||||
TopicSummaryView primaryTopic,
|
||||
List<TagSummaryView> tags,
|
||||
ProjectSummaryView primaryProject,
|
||||
AssetReferenceView coverAsset,
|
||||
Instant publishedAt,
|
||||
Instant updatedAt,
|
||||
Instant lastVerifiedAt) {
|
||||
|
||||
public 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);
|
||||
tags = tags == null ? List.of() : List.copyOf(tags);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ProjectDetailResponse.project}. */
|
||||
public record PublishedProjectView(
|
||||
String name,
|
||||
String slug,
|
||||
String oneLinePurpose,
|
||||
String purpose,
|
||||
String boundary,
|
||||
String phase,
|
||||
String currentObjective,
|
||||
String nextStep,
|
||||
String systemOverviewMarkdown,
|
||||
List<String> technologies,
|
||||
Instant updatedAt) {
|
||||
|
||||
public PublishedProjectView {
|
||||
technologies = technologies == null ? List.of() : List.copyOf(technologies);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code QuestionDetailResponse.question}. */
|
||||
public record PublishedQuestionView(
|
||||
String question,
|
||||
String summary,
|
||||
String context,
|
||||
String importance,
|
||||
String status,
|
||||
String nextVerification,
|
||||
QuestionPointGroupView points,
|
||||
List<QuestionUpdateView> updates,
|
||||
String resolutionType,
|
||||
String resolutionSummary,
|
||||
Instant resolvedAt,
|
||||
Instant openedAt,
|
||||
Instant updatedAt) {
|
||||
|
||||
public PublishedQuestionView {
|
||||
updates = updates == null ? List.of() : List.copyOf(updates);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code QuestionDetailResponse}. */
|
||||
public record QuestionDetailView(
|
||||
String canonicalPath,
|
||||
boolean indexable,
|
||||
PublishedQuestionView question,
|
||||
QuestionRelationsView relations) {}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code QuestionListItem}. */
|
||||
public record QuestionListItemView(
|
||||
String question,
|
||||
String path,
|
||||
String status,
|
||||
String summary,
|
||||
String currentUnderstanding,
|
||||
String nextVerification,
|
||||
ProjectSummaryView primaryProject,
|
||||
Instant updatedAt) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code QuestionPage}. */
|
||||
public record QuestionPageView(List<QuestionListItemView> items, PageMetadataView page) {
|
||||
|
||||
public QuestionPageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code QuestionPointGroup}. */
|
||||
public record QuestionPointGroupView(
|
||||
List<String> facts, List<String> assumptions, List<String> unknowns, List<String> constraints) {
|
||||
|
||||
public QuestionPointGroupView {
|
||||
facts = facts == null ? List.of() : List.copyOf(facts);
|
||||
assumptions = assumptions == null ? List.of() : List.copyOf(assumptions);
|
||||
unknowns = unknowns == null ? List.of() : List.copyOf(unknowns);
|
||||
constraints = constraints == null ? List.of() : List.copyOf(constraints);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code QuestionDetailResponse.relations}. */
|
||||
public record QuestionRelationsView(
|
||||
RelatedEntryView primaryProject,
|
||||
RelatedEntryView resultCase,
|
||||
RelatedEntryView producedDecision,
|
||||
List<RelatedEntryView> derivedReferences) {
|
||||
|
||||
public QuestionRelationsView {
|
||||
derivedReferences = derivedReferences == null ? List.of() : List.copyOf(derivedReferences);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** 계약 {@code QuestionUpdatePublic}. 공개된 조사 기록 한 건이다. */
|
||||
public record QuestionUpdateView(
|
||||
String type, String title, String bodyMarkdown, Instant occurredAt) {}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code ReferenceDetailResponse}. */
|
||||
public record ReferenceDetailView(
|
||||
String canonicalPath,
|
||||
boolean indexable,
|
||||
PublishedDocumentView document,
|
||||
ReferenceRelationsView relations) {}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ReferenceDetailResponse.relations}. Case 의 관계와 이름·구성이 다르다. */
|
||||
public record ReferenceRelationsView(
|
||||
List<RelatedEntryView> supportingCases,
|
||||
List<RelatedEntryView> relatedDecisions,
|
||||
List<RelatedEntryView> relatedReferences) {
|
||||
|
||||
public ReferenceRelationsView {
|
||||
supportingCases = supportingCases == null ? List.of() : List.copyOf(supportingCases);
|
||||
relatedDecisions = relatedDecisions == null ? List.of() : List.copyOf(relatedDecisions);
|
||||
relatedReferences = relatedReferences == null ? List.of() : List.copyOf(relatedReferences);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code RelatedEntry}. */
|
||||
public record RelatedEntryView(String type, String title, String summary, String path) {}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ReleaseDetailResponse}. */
|
||||
public record ReleaseDetailView(
|
||||
String version,
|
||||
String title,
|
||||
String summary,
|
||||
LocalDate releasedOn,
|
||||
List<String> changeTypes,
|
||||
String reasonMarkdown,
|
||||
String changesMarkdown,
|
||||
String userImpactMarkdown,
|
||||
String implementationImpactMarkdown,
|
||||
String verificationMarkdown,
|
||||
String knownLimitationsMarkdown,
|
||||
List<RelatedEntryView> relatedRecords) {
|
||||
|
||||
public ReleaseDetailView {
|
||||
changeTypes = changeTypes == null ? List.of() : List.copyOf(changeTypes);
|
||||
relatedRecords = relatedRecords == null ? List.of() : List.copyOf(relatedRecords);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code ReleaseListItem}. */
|
||||
public record ReleaseListItemView(
|
||||
String version,
|
||||
String title,
|
||||
String summary,
|
||||
LocalDate releasedOn,
|
||||
List<String> changeTypes,
|
||||
String path) {
|
||||
|
||||
public ReleaseListItemView {
|
||||
changeTypes = changeTypes == null ? List.of() : List.copyOf(changeTypes);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code SearchResultItem}. */
|
||||
public record SearchResultItemView(
|
||||
String contentType,
|
||||
String title,
|
||||
String path,
|
||||
String snippet,
|
||||
List<String> matchedFields,
|
||||
TopicSummaryView primaryTopic,
|
||||
ProjectSummaryView primaryProject,
|
||||
Instant publishedAt,
|
||||
Instant updatedAt) {
|
||||
|
||||
public SearchResultItemView {
|
||||
matchedFields = matchedFields == null ? List.of() : List.copyOf(matchedFields);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code SearchResultPage}. */
|
||||
public record SearchResultPageView(
|
||||
String query, List<SearchResultItemView> items, PageMetadataView page) {
|
||||
|
||||
public SearchResultPageView {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code SiteResponse}. */
|
||||
public record SiteView(
|
||||
String brandTitle,
|
||||
String identityStatement,
|
||||
String operatorDisplayName,
|
||||
String operatorShortIdentity,
|
||||
AssetReferenceView operatorAvatar,
|
||||
String operatorProfilePath,
|
||||
List<ContactLinkView> contacts) {
|
||||
|
||||
public SiteView {
|
||||
contacts = contacts == null ? List.of() : List.copyOf(contacts);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code TagSummary}. */
|
||||
public record TagSummaryView(String name, String slug) {}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 계약 {@code TopicDetailResponse}. */
|
||||
public record TopicDetailView(
|
||||
String name,
|
||||
String slug,
|
||||
String description,
|
||||
String scope,
|
||||
RelatedEntryView featuredReference,
|
||||
List<RelatedEntryView> featuredCases,
|
||||
List<RelatedEntryView> activeQuestions,
|
||||
List<RelatedEntryView> relatedProjects,
|
||||
List<LatestEntryView> latestRecords) {
|
||||
|
||||
public TopicDetailView {
|
||||
featuredCases = featuredCases == null ? List.of() : List.copyOf(featuredCases);
|
||||
activeQuestions = activeQuestions == null ? List.of() : List.copyOf(activeQuestions);
|
||||
relatedProjects = relatedProjects == null ? List.of() : List.copyOf(relatedProjects);
|
||||
latestRecords = latestRecords == null ? List.of() : List.copyOf(latestRecords);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code TopicListItem}. */
|
||||
public record TopicListItemView(String name, String slug, String description, int recordCount) {}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.model;
|
||||
|
||||
/** 계약 {@code TopicSummary}. */
|
||||
public record TopicSummaryView(String name, String slug) {}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.CaseDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.QuestionDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReferenceDetailView;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 공개된 기록 상세.
|
||||
*
|
||||
* <p>본문은 {@code public_resource_projection.payload}(studio 렌더 모델)가 아니라 원본 테이블에서 읽는다 — 공개 계약의 상세 모양은
|
||||
* 렌더 모델과 다르다(본문이 블록 배열이 아니라 Markdown 원문이고, environmentSummary 가 배열이며, tags/coverAsset 과 유형별 관계가 따로
|
||||
* 있다). projection 은 "무엇이 공개됐는가"와 게시 시각을 정하는 데 쓴다.
|
||||
*/
|
||||
public interface PublicDocumentQueryPort {
|
||||
|
||||
Optional<CaseDetailView> findCase(String slug);
|
||||
|
||||
Optional<ReferenceDetailView> findReference(String slug);
|
||||
|
||||
Optional<QuestionDetailView> findQuestion(String slug);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.KnowledgePageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.QuestionPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ExploreKnowledgeQuery;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ExploreQuestionsQuery;
|
||||
|
||||
/** 탐색 목록. 공개된 것만 본다 — {@code public_resource_projection.publication_state = 'ACTIVE'}. */
|
||||
public interface PublicExploreQueryPort {
|
||||
|
||||
KnowledgePageView knowledge(ExploreKnowledgeQuery query);
|
||||
|
||||
QuestionPageView questions(ExploreQuestionsQuery query);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectDecisionPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectListItemView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectRecordPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectDecisionPageQuery;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectPageQuery;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectRecordPageQuery;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** 프로젝트 목록·상세와 그 하위 목록. */
|
||||
public interface PublicProjectQueryPort {
|
||||
|
||||
List<ProjectListItemView> list();
|
||||
|
||||
Optional<ProjectDetailView> findBySlug(String slug);
|
||||
|
||||
/** 프로젝트가 없으면 {@link Optional#empty()} — 빈 페이지와 404 를 호출자가 구분해야 한다. */
|
||||
Optional<ProjectDecisionPageView> decisions(ProjectDecisionPageQuery query);
|
||||
|
||||
Optional<ProjectRecordPageView> records(ProjectRecordPageQuery query);
|
||||
|
||||
Optional<ProjectActivityPageView> activities(ProjectPageQuery query);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReleaseDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReleaseListItemView;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** 릴리스 목록·상세. 공개된 것({@code workflow_status = 'PUBLISHED'})만 본다. */
|
||||
public interface PublicReleaseQueryPort {
|
||||
|
||||
List<ReleaseListItemView> list();
|
||||
|
||||
Optional<ReleaseDetailView> findByVersion(String version);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.SearchResultPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SearchQuery;
|
||||
|
||||
/** 공개 검색. {@code public_resource_projection.search_text} 를 본다. */
|
||||
@FunctionalInterface
|
||||
public interface PublicSearchQueryPort {
|
||||
|
||||
SearchResultPageView search(SearchQuery query);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.HomeView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProfileView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.SiteView;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 사이트 정체성 · 홈 · 프로필.
|
||||
*
|
||||
* <p>이 셋의 원천은 단일 행 테이블({@code site_config} / {@code home_focus_config} / {@code profile_page})이고 편집
|
||||
* API 는 {@code studio-management-v1}(범위 밖)이 소유한다. 지금은 V9 의 시딩이 유일한 공급원이며, 프로필은 공개로 전환되기 전까지 비어 있을 수
|
||||
* 있어 {@link Optional} 이다.
|
||||
*/
|
||||
public interface PublicSiteQueryPort {
|
||||
|
||||
Optional<SiteView> site();
|
||||
|
||||
HomeView home(int latestEntryLimit);
|
||||
|
||||
Optional<ProfileView> profile();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.port.out;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.model.TopicDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.TopicListItemView;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** 주제 목록·상세. */
|
||||
public interface PublicTopicQueryPort {
|
||||
|
||||
List<TopicListItemView> list();
|
||||
|
||||
Optional<TopicDetailView> findBySlug(String slug);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/** 파라미터가 없는 조회({@code getPublicSite} / {@code getPublicHome} / 목록 전체 등). */
|
||||
public record EmptyQuery() implements Query {}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/**
|
||||
* {@code exploreKnowledge} 의 입력.
|
||||
*
|
||||
* @param type CASE / REFERENCE. null 이면 둘 다
|
||||
* @param sort PUBLISHED_DESC / UPDATED_DESC / VERIFIED_DESC
|
||||
*/
|
||||
public record ExploreKnowledgeQuery(
|
||||
String type,
|
||||
String topicSlug,
|
||||
String projectSlug,
|
||||
String tagSlug,
|
||||
Integer year,
|
||||
String sort,
|
||||
PublicPageRequest page)
|
||||
implements Query {}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/**
|
||||
* {@code exploreQuestions} 의 입력.
|
||||
*
|
||||
* @param status OPEN / INVESTIGATING / PAUSED / RESOLVED. null 이면 전부
|
||||
* @param sort UPDATED_DESC / OPENED_DESC / RESOLVED_DESC
|
||||
*/
|
||||
public record ExploreQuestionsQuery(
|
||||
String status,
|
||||
String topicSlug,
|
||||
String projectSlug,
|
||||
String tagSlug,
|
||||
String sort,
|
||||
PublicPageRequest page)
|
||||
implements Query {}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/**
|
||||
* {@code listPublicProjectDecisions} 의 입력.
|
||||
*
|
||||
* <p>계약이 이 operation 에만 주는 {@code status} 필터 때문에 {@link ProjectPageQuery} 와 나눈다 — 하나의 record 에 세
|
||||
* operation 의 필터를 다 담으면 어느 필드가 어느 operation 에서 무시되는지 타입으로 알 수 없다.
|
||||
*
|
||||
* @param status 계약이 enum 을 두지 않은 자유 문자열이다 — 도메인의 decision lifecycle 값이 그대로 들어온다
|
||||
*/
|
||||
public record ProjectDecisionPageQuery(String projectSlug, String status, PublicPageRequest page)
|
||||
implements Query {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/** {@code listPublicProjectActivities} 의 입력. 계약이 이 operation 에는 필터를 두지 않았다. */
|
||||
public record ProjectPageQuery(String projectSlug, PublicPageRequest page) implements Query {}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/**
|
||||
* {@code listPublicProjectRecords} 의 입력.
|
||||
*
|
||||
* @param type CASE / REFERENCE / QUESTION. null 이면 셋 다
|
||||
* @param relation PRIMARY / RELATED. null 이면 둘 다
|
||||
*/
|
||||
public record ProjectRecordPageQuery(
|
||||
String projectSlug, String type, String relation, PublicPageRequest page) implements Query {}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.error.PublicError;
|
||||
import dev.caskeleton.application.techlog.publicsite.error.PublicException;
|
||||
|
||||
/**
|
||||
* 계약이 모든 목록에 쓰는 {@code page}/{@code size}. 1-based 다.
|
||||
*
|
||||
* <p>검증을 한 곳에 모은 이유는 여섯 개 목록 operation 이 같은 규칙을 쓰기 때문이다 — 각자 검사하면 어느 하나가 한계를 빠뜨려도 드러나지 않는다.
|
||||
*/
|
||||
public record PublicPageRequest(int page, int size) {
|
||||
|
||||
private static final int MAX_SIZE = 100;
|
||||
|
||||
public PublicPageRequest {
|
||||
if (page < 1) {
|
||||
throw PublicException.of(PublicError.PUBLIC_REQUEST_INVALID, "page must be at least 1");
|
||||
}
|
||||
if (size < 1 || size > MAX_SIZE) {
|
||||
throw PublicException.of(
|
||||
PublicError.PUBLIC_REQUEST_INVALID, "size must be between 1 and " + MAX_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
public int offset() {
|
||||
return (page - 1) * size;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/** {@code searchPublicResources} 의 입력. */
|
||||
public record SearchQuery(String query, String type, String topicSlug, PublicPageRequest page)
|
||||
implements Query {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.query;
|
||||
|
||||
import dev.caskeleton.application.query.Query;
|
||||
|
||||
/** slug 하나로 조회하는 operation 들의 공통 입력. */
|
||||
public record SlugQuery(String slug) implements Query {}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.KnowledgePageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicExploreQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ExploreKnowledgeQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code exploreKnowledge}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ExploreKnowledgeUseCase
|
||||
implements QueryUseCase<ExploreKnowledgeQuery, KnowledgePageView> {
|
||||
|
||||
private final PublicExploreQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ExploreKnowledgeUseCase(PublicExploreQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgePageView handle(ExploreKnowledgeQuery input) {
|
||||
return transactions.inRead(() -> port.knowledge(input));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.QuestionPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicExploreQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ExploreQuestionsQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code exploreQuestions}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ExploreQuestionsUseCase
|
||||
implements QueryUseCase<ExploreQuestionsQuery, QuestionPageView> {
|
||||
|
||||
private final PublicExploreQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ExploreQuestionsUseCase(PublicExploreQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuestionPageView handle(ExploreQuestionsQuery input) {
|
||||
return transactions.inRead(() -> port.questions(input));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.CaseDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicDocumentQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicCase}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicCaseUseCase implements QueryUseCase<SlugQuery, CaseDetailView> {
|
||||
|
||||
private final PublicDocumentQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicCaseUseCase(PublicDocumentQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CaseDetailView handle(SlugQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() -> port.findCase(slug).orElseThrow(() -> PublicReadUseCases.notFound("case " + slug)));
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.HomeView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicSiteQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicHome}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicHomeUseCase implements QueryUseCase<EmptyQuery, HomeView> {
|
||||
|
||||
/** 계약 {@code HomeResponse.latestEntries} 는 최신 목록이며 화면이 한 화면에 담는 개수다. */
|
||||
private static final int LATEST_ENTRY_LIMIT = 10;
|
||||
|
||||
private final PublicSiteQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicHomeUseCase(PublicSiteQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeView handle(EmptyQuery input) {
|
||||
return transactions.inRead(() -> port.home(LATEST_ENTRY_LIMIT));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProfileView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicSiteQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicProfile}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicProfileUseCase implements QueryUseCase<EmptyQuery, ProfileView> {
|
||||
|
||||
private final PublicSiteQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicProfileUseCase(PublicSiteQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileView handle(EmptyQuery input) {
|
||||
return transactions.inRead(
|
||||
() -> port.profile().orElseThrow(() -> PublicReadUseCases.notFound("the profile page")));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicProject}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicProjectUseCase implements QueryUseCase<SlugQuery, ProjectDetailView> {
|
||||
|
||||
private final PublicProjectQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicProjectUseCase(PublicProjectQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectDetailView handle(SlugQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.findBySlug(slug)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("project " + slug)));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.QuestionDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicDocumentQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicQuestion}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicQuestionUseCase implements QueryUseCase<SlugQuery, QuestionDetailView> {
|
||||
|
||||
private final PublicDocumentQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicQuestionUseCase(PublicDocumentQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuestionDetailView handle(SlugQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.findQuestion(slug)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("question " + slug)));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReferenceDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicDocumentQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicReference}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicReferenceUseCase
|
||||
implements QueryUseCase<SlugQuery, ReferenceDetailView> {
|
||||
|
||||
private final PublicDocumentQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicReferenceUseCase(PublicDocumentQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReferenceDetailView handle(SlugQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.findReference(slug)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("reference " + slug)));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReleaseDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicReleaseQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicRelease}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicReleaseUseCase implements QueryUseCase<SlugQuery, ReleaseDetailView> {
|
||||
|
||||
private final PublicReleaseQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicReleaseUseCase(PublicReleaseQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReleaseDetailView handle(SlugQuery input) {
|
||||
String version = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.findByVersion(version)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("release " + version)));
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.SiteView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicSiteQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicSite}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicSiteUseCase implements QueryUseCase<EmptyQuery, SiteView> {
|
||||
|
||||
private final PublicSiteQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicSiteUseCase(PublicSiteQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SiteView handle(EmptyQuery input) {
|
||||
return transactions.inRead(
|
||||
() -> port.site().orElseThrow(() -> PublicReadUseCases.notFound("the site profile")));
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.TopicDetailView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicTopicQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SlugQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code getPublicTopic}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class GetPublicTopicUseCase implements QueryUseCase<SlugQuery, TopicDetailView> {
|
||||
|
||||
private final PublicTopicQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public GetPublicTopicUseCase(PublicTopicQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopicDetailView handle(SlugQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.slug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.findBySlug(slug).orElseThrow(() -> PublicReadUseCases.notFound("topic " + slug)));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectPageQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicProjectActivities}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicProjectActivitiesUseCase
|
||||
implements QueryUseCase<ProjectPageQuery, ProjectActivityPageView> {
|
||||
|
||||
private final PublicProjectQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicProjectActivitiesUseCase(
|
||||
PublicProjectQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectActivityPageView handle(ProjectPageQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.projectSlug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.activities(input)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("project " + slug)));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectDecisionPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectDecisionPageQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicProjectDecisions}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicProjectDecisionsUseCase
|
||||
implements QueryUseCase<ProjectDecisionPageQuery, ProjectDecisionPageView> {
|
||||
|
||||
private final PublicProjectQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicProjectDecisionsUseCase(
|
||||
PublicProjectQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectDecisionPageView handle(ProjectDecisionPageQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.projectSlug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.decisions(input)
|
||||
.orElseThrow(() -> PublicReadUseCases.notFound("project " + slug)));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectRecordPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectRecordPageQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicProjectRecords}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicProjectRecordsUseCase
|
||||
implements QueryUseCase<ProjectRecordPageQuery, ProjectRecordPageView> {
|
||||
|
||||
private final PublicProjectQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicProjectRecordsUseCase(
|
||||
PublicProjectQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectRecordPageView handle(ProjectRecordPageQuery input) {
|
||||
String slug = PublicReadUseCases.requireSlug(input.projectSlug());
|
||||
return transactions.inRead(
|
||||
() ->
|
||||
port.records(input).orElseThrow(() -> PublicReadUseCases.notFound("project " + slug)));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectListItemView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicProjects}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicProjectsUseCase
|
||||
implements QueryUseCase<EmptyQuery, List<ProjectListItemView>> {
|
||||
|
||||
private final PublicProjectQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicProjectsUseCase(PublicProjectQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProjectListItemView> handle(EmptyQuery input) {
|
||||
return transactions.inRead(port::list);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.ReleaseListItemView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicReleaseQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicReleases}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicReleasesUseCase
|
||||
implements QueryUseCase<EmptyQuery, List<ReleaseListItemView>> {
|
||||
|
||||
private final PublicReleaseQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicReleasesUseCase(PublicReleaseQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReleaseListItemView> handle(EmptyQuery input) {
|
||||
return transactions.inRead(port::list);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.TopicListItemView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicTopicQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.EmptyQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code listPublicTopics}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class ListPublicTopicsUseCase
|
||||
implements QueryUseCase<EmptyQuery, List<TopicListItemView>> {
|
||||
|
||||
private final PublicTopicQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public ListPublicTopicsUseCase(PublicTopicQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TopicListItemView> handle(EmptyQuery input) {
|
||||
return transactions.inRead(port::list);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.techlog.publicsite.error.PublicError;
|
||||
import dev.caskeleton.application.techlog.publicsite.error.PublicException;
|
||||
|
||||
/**
|
||||
* 공개 조회 use case 들이 공유하는 조각.
|
||||
*
|
||||
* <p>18개 operation 이 전부 읽기 전용이고 도메인 규칙이 없다. 각자 "없으면 404" 를 따로 쓰면 문구와 코드가 갈라지므로 한 곳에 둔다.
|
||||
*/
|
||||
final class PublicReadUseCases {
|
||||
|
||||
private PublicReadUseCases() {}
|
||||
|
||||
static PublicException notFound(String what) {
|
||||
return PublicException.of(PublicError.PUBLIC_RESOURCE_NOT_FOUND, what + " is not published");
|
||||
}
|
||||
|
||||
static String requireSlug(String slug) {
|
||||
if (slug == null || slug.isBlank()) {
|
||||
throw PublicException.of(PublicError.PUBLIC_REQUEST_INVALID, "a slug is required");
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package dev.caskeleton.application.techlog.publicsite.service;
|
||||
|
||||
import dev.caskeleton.application.capability.Idempotency;
|
||||
import dev.caskeleton.application.capability.RepositoryAccess;
|
||||
import dev.caskeleton.application.capability.UseCaseCapability;
|
||||
import dev.caskeleton.application.techlog.publicsite.model.SearchResultPageView;
|
||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicSearchQueryPort;
|
||||
import dev.caskeleton.application.techlog.publicsite.query.SearchQuery;
|
||||
import dev.caskeleton.application.transaction.TransactionMode;
|
||||
import dev.caskeleton.application.transaction.TransactionPort;
|
||||
import dev.caskeleton.application.usecase.QueryUseCase;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 계약 {@code searchPublicResources}. 공개 조회이며 인증이 없다.
|
||||
*
|
||||
* <p>공개된 것만 보여준다 — 어떤 경로로도 Working Copy 나 검증/미리보기 artifact 에 닿지 않는다 (계약 서문 "이 계약이 반환하지 않는 것").
|
||||
*/
|
||||
@UseCaseCapability(
|
||||
transactionMode = TransactionMode.READ_ONLY,
|
||||
idempotency = Idempotency.IDEMPOTENT,
|
||||
repositoryAccess = RepositoryAccess.READ_REPOSITORY)
|
||||
public final class SearchPublicResourcesUseCase
|
||||
implements QueryUseCase<SearchQuery, SearchResultPageView> {
|
||||
|
||||
private final PublicSearchQueryPort port;
|
||||
private final TransactionPort transactions;
|
||||
|
||||
public SearchPublicResourcesUseCase(PublicSearchQueryPort port, TransactionPort transactions) {
|
||||
this.port = Objects.requireNonNull(port, "port");
|
||||
this.transactions = Objects.requireNonNull(transactions, "transactions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResultPageView handle(SearchQuery input) {
|
||||
return transactions.inRead(() -> port.search(input));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user