feat: 프로젝트 주제를 저장하고 공개 응답에 싣는다
프로젝트 화면의 "주요 주제" 가 늘 비어 있었다. `project_topic` 은 테이블도 있고 공개 조회가 조인할 수도 있었지만, 응답에 실을 자리가 없었고 저장할 경로도 없었다 — `ProjectUpdateRequest.topicIds` 는 계약에 있었지만 명령이 그 값을 들고 다니지 않았다. 주제는 통째로 교체한다. 부분 수정으로 두면 "주제를 전부 뗀다" 를 표현할 방법이 없고, 화면도 목록 하나를 한 번에 저장하므로 그쪽과도 맞는다. 고른 순서가 곧 화면 순서이므로 목록의 자리를 display_order 에 그대로 적는다. 공개 조회는 ACTIVE 인 주제만 내보낸다 — 보관된 주제를 링크로 내보내면 따라간 곳이 비어 있다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
This commit is contained in:
co-authored by
Claude Opus 5
parent
ca63d7d3ec
commit
6aa140077d
+1
@@ -109,6 +109,7 @@ public class ManagementProjectController {
|
|||||||
body.getCurrentObjective(),
|
body.getCurrentObjective(),
|
||||||
body.getNextStep(),
|
body.getNextStep(),
|
||||||
labels,
|
labels,
|
||||||
|
body.getTopicIds() == null ? List.of() : List.copyOf(body.getTopicIds()),
|
||||||
body.getTargetVisibility().getValue(),
|
body.getTargetVisibility().getValue(),
|
||||||
body.getFeaturedOrder(),
|
body.getFeaturedOrder(),
|
||||||
ManagementPrincipals.require(principal))));
|
ManagementPrincipals.require(principal))));
|
||||||
|
|||||||
+1
-1
@@ -89,7 +89,7 @@ public final class ManagementResponseMapper {
|
|||||||
model.setNextStep(view.nextStep());
|
model.setNextStep(view.nextStep());
|
||||||
model.setTechnologyLabels(List.copyOf(view.technologyLabels()));
|
model.setTechnologyLabels(List.copyOf(view.technologyLabels()));
|
||||||
model.setFeaturedOrder(view.featuredOrder());
|
model.setFeaturedOrder(view.featuredOrder());
|
||||||
model.setTopicIds(List.of());
|
model.setTopicIds(List.copyOf(view.topicIds()));
|
||||||
model.setDocumentLinks(List.of());
|
model.setDocumentLinks(List.of());
|
||||||
model.setQuestionLinks(List.of());
|
model.setQuestionLinks(List.of());
|
||||||
return model;
|
return model;
|
||||||
|
|||||||
+3
@@ -9,6 +9,7 @@ import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectDet
|
|||||||
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectListItem;
|
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectListItem;
|
||||||
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectListResponse;
|
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectListResponse;
|
||||||
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectRecordPage;
|
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.ProjectRecordPage;
|
||||||
|
import dev.caskeleton.adapter.inbound.web.techlog.publicapi.api.model.TopicSummary;
|
||||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityItemView;
|
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityItemView;
|
||||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityPageView;
|
import dev.caskeleton.application.techlog.publicsite.model.ProjectActivityPageView;
|
||||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectDecisionItemView;
|
import dev.caskeleton.application.techlog.publicsite.model.ProjectDecisionItemView;
|
||||||
@@ -56,6 +57,8 @@ public final class ProjectResponseMapper {
|
|||||||
body.setNextStep(p.nextStep());
|
body.setNextStep(p.nextStep());
|
||||||
body.setSystemOverviewMarkdown(p.systemOverviewMarkdown());
|
body.setSystemOverviewMarkdown(p.systemOverviewMarkdown());
|
||||||
body.setTechnologies(p.technologies());
|
body.setTechnologies(p.technologies());
|
||||||
|
body.setTopics(
|
||||||
|
p.topics().stream().map(topic -> new TopicSummary(topic.name(), topic.slug())).toList());
|
||||||
body.setUpdatedAt(PublicResponseMapper.at(p.updatedAt()));
|
body.setUpdatedAt(PublicResponseMapper.at(p.updatedAt()));
|
||||||
|
|
||||||
ProjectDetailResponse dto = new ProjectDetailResponse();
|
ProjectDetailResponse dto = new ProjectDetailResponse();
|
||||||
|
|||||||
+39
-1
@@ -93,6 +93,7 @@ public class JdbcProjectRepositoryAdapter implements ProjectRepositoryPort {
|
|||||||
rs.getString("current_objective"),
|
rs.getString("current_objective"),
|
||||||
rs.getString("next_step"),
|
rs.getString("next_step"),
|
||||||
labels(rs.getString("technology_labels")),
|
labels(rs.getString("technology_labels")),
|
||||||
|
topicIdsOf(rs.getObject("id", UUID.class)),
|
||||||
rs.getString("workflow_status"),
|
rs.getString("workflow_status"),
|
||||||
rs.getString("target_visibility"),
|
rs.getString("target_visibility"),
|
||||||
(Integer) rs.getObject("featured_order"),
|
(Integer) rs.getObject("featured_order"),
|
||||||
@@ -190,7 +191,44 @@ public class JdbcProjectRepositoryAdapter implements ProjectRepositoryPort {
|
|||||||
.param("featured", command.featuredOrder())
|
.param("featured", command.featuredOrder())
|
||||||
.param("actor", command.actor())
|
.param("actor", command.actor())
|
||||||
.update();
|
.update();
|
||||||
return updated == 0 ? Optional.empty() : find(command.id());
|
if (updated == 0) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
replaceTopics(command.id(), command.topicIds());
|
||||||
|
return find(command.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 편집기가 고른 주제. 순서가 곧 화면 순서이므로 목록의 자리를 {@code display_order} 에 그대로 적는다. */
|
||||||
|
private List<UUID> topicIdsOf(UUID projectId) {
|
||||||
|
return jdbcClient
|
||||||
|
.sql(
|
||||||
|
"SELECT topic_id FROM project_topic WHERE project_id = :projectId"
|
||||||
|
+ " ORDER BY display_order")
|
||||||
|
.param("projectId", projectId)
|
||||||
|
.query(UUID.class)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 통째로 교체한다. 부분 수정으로 두면 "주제를 전부 뗀다" 를 표현할 방법이 없고, 화면도 목록 하나를
|
||||||
|
* 한 번에 저장하므로 그쪽과도 맞는다.
|
||||||
|
*/
|
||||||
|
private void replaceTopics(UUID projectId, List<UUID> topicIds) {
|
||||||
|
jdbcClient
|
||||||
|
.sql("DELETE FROM project_topic WHERE project_id = :projectId")
|
||||||
|
.param("projectId", projectId)
|
||||||
|
.update();
|
||||||
|
int order = 0;
|
||||||
|
for (UUID topicId : topicIds) {
|
||||||
|
jdbcClient
|
||||||
|
.sql(
|
||||||
|
"INSERT INTO project_topic (project_id, topic_id, display_order)"
|
||||||
|
+ " VALUES (:projectId, :topicId, :order)")
|
||||||
|
.param("projectId", projectId)
|
||||||
|
.param("topicId", topicId)
|
||||||
|
.param("order", order++)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String nullToEmpty(String value) {
|
private static String nullToEmpty(String value) {
|
||||||
|
|||||||
+20
@@ -10,6 +10,7 @@ import dev.caskeleton.application.techlog.publicsite.model.ProjectListItemView;
|
|||||||
import dev.caskeleton.application.techlog.publicsite.model.ProjectRecordPageView;
|
import dev.caskeleton.application.techlog.publicsite.model.ProjectRecordPageView;
|
||||||
import dev.caskeleton.application.techlog.publicsite.model.PublishedProjectView;
|
import dev.caskeleton.application.techlog.publicsite.model.PublishedProjectView;
|
||||||
import dev.caskeleton.application.techlog.publicsite.model.RelatedEntryView;
|
import dev.caskeleton.application.techlog.publicsite.model.RelatedEntryView;
|
||||||
|
import dev.caskeleton.application.techlog.publicsite.model.TopicSummaryView;
|
||||||
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
import dev.caskeleton.application.techlog.publicsite.port.out.PublicProjectQueryPort;
|
||||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectDecisionPageQuery;
|
import dev.caskeleton.application.techlog.publicsite.query.ProjectDecisionPageQuery;
|
||||||
import dev.caskeleton.application.techlog.publicsite.query.ProjectPageQuery;
|
import dev.caskeleton.application.techlog.publicsite.query.ProjectPageQuery;
|
||||||
@@ -88,6 +89,7 @@ public class JdbcPublicProjectQueryAdapter implements PublicProjectQueryPort {
|
|||||||
rs.getString("next_step"),
|
rs.getString("next_step"),
|
||||||
rs.getString("system_overview_markdown"),
|
rs.getString("system_overview_markdown"),
|
||||||
json.strings(rs.getString("technology_labels")),
|
json.strings(rs.getString("technology_labels")),
|
||||||
|
topicsOf(projectId),
|
||||||
JdbcPublicDocumentQueryAdapter.instant(rs, "updated_at"));
|
JdbcPublicDocumentQueryAdapter.instant(rs, "updated_at"));
|
||||||
return new ProjectDetailView(
|
return new ProjectDetailView(
|
||||||
rs.getString("navigation_path"),
|
rs.getString("navigation_path"),
|
||||||
@@ -327,4 +329,22 @@ public class JdbcPublicProjectQueryAdapter implements PublicProjectQueryPort {
|
|||||||
.query(UUID.class)
|
.query(UUID.class)
|
||||||
.optional();
|
.optional();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프로젝트가 다루는 주제. 화면의 "주요 주제" 가 이 목록을 그린다.
|
||||||
|
*
|
||||||
|
* <p>{@code ACTIVE} 인 주제만 내보낸다 — 보관된 주제를 링크로 내보내면 따라간 곳이 비어 있다. 순서는 편집기가 정한 {@code display_order}
|
||||||
|
* 를 그대로 따른다.
|
||||||
|
*/
|
||||||
|
private List<TopicSummaryView> topicsOf(UUID projectId) {
|
||||||
|
return jdbcClient
|
||||||
|
.sql(
|
||||||
|
"SELECT t.name, t.slug FROM project_topic pt"
|
||||||
|
+ " JOIN topic t ON t.id = pt.topic_id"
|
||||||
|
+ " WHERE pt.project_id = :projectId AND t.status = 'ACTIVE'"
|
||||||
|
+ " ORDER BY pt.display_order, t.name")
|
||||||
|
.param("projectId", projectId)
|
||||||
|
.query((rs, rowNum) -> new TopicSummaryView(rs.getString("name"), rs.getString("slug")))
|
||||||
|
.list();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+91
@@ -9,6 +9,7 @@ import dev.caskeleton.application.techlog.management.command.CreateProjectActivi
|
|||||||
import dev.caskeleton.application.techlog.management.command.CreateProjectCommand;
|
import dev.caskeleton.application.techlog.management.command.CreateProjectCommand;
|
||||||
import dev.caskeleton.application.techlog.management.command.UpdateHomeFocusCommand;
|
import dev.caskeleton.application.techlog.management.command.UpdateHomeFocusCommand;
|
||||||
import dev.caskeleton.application.techlog.management.command.UpdateProjectActivityCommand;
|
import dev.caskeleton.application.techlog.management.command.UpdateProjectActivityCommand;
|
||||||
|
import dev.caskeleton.application.techlog.management.command.UpdateProjectCommand;
|
||||||
import dev.caskeleton.application.techlog.management.model.HomeFocusConfigView;
|
import dev.caskeleton.application.techlog.management.model.HomeFocusConfigView;
|
||||||
import dev.caskeleton.application.techlog.management.model.ProjectActivityView;
|
import dev.caskeleton.application.techlog.management.model.ProjectActivityView;
|
||||||
import dev.caskeleton.application.techlog.management.model.ProjectEditView;
|
import dev.caskeleton.application.techlog.management.model.ProjectEditView;
|
||||||
@@ -694,4 +695,94 @@ class ManagementPersistenceIntegrationTest {
|
|||||||
assertThat(activities.delete(project.id(), activity.id(), activity.version())).isEqualTo(1);
|
assertThat(activities.delete(project.id(), activity.id(), activity.version())).isEqualTo(1);
|
||||||
assertThat(activities.listByProject(project.id())).isEmpty();
|
assertThat(activities.listByProject(project.id())).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 프로젝트 주제
|
||||||
|
|
||||||
|
private static UUID insertTopic(String name, String slug) {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
jdbcClient
|
||||||
|
.sql(
|
||||||
|
"INSERT INTO topic (id, name, normalized_name, slug, created_by, updated_by)"
|
||||||
|
+ " VALUES (:id, :name, :normalized, :slug, 'test', 'test')")
|
||||||
|
.param("id", id)
|
||||||
|
.param("name", name)
|
||||||
|
.param("normalized", name.toLowerCase(java.util.Locale.ROOT))
|
||||||
|
.param("slug", slug)
|
||||||
|
.update();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdateProjectCommand updateWithTopics(
|
||||||
|
ProjectEditView project, java.util.List<UUID> topicIds) {
|
||||||
|
return new UpdateProjectCommand(
|
||||||
|
project.id(),
|
||||||
|
project.version(),
|
||||||
|
project.name(),
|
||||||
|
project.slug() == null ? "project-" + project.id() : project.slug(),
|
||||||
|
"목적 한 줄",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
project.phase(),
|
||||||
|
"지금 목표",
|
||||||
|
"다음 작업",
|
||||||
|
java.util.List.of("Keycloak"),
|
||||||
|
topicIds,
|
||||||
|
"PRIVATE",
|
||||||
|
null,
|
||||||
|
"test");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프로젝트 화면의 "주요 주제" 는 이 링크 테이블을 읽는다. 화면은 처음부터 주제를 읽고 있었지만 저장할 경로가 없어 늘 비어 있었다 — 여기서 확인하는 것은 고른 순서가
|
||||||
|
* 그대로 남는가와, 통째로 교체되는가다.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void projectTopicsRoundTripInTheChosenOrder() {
|
||||||
|
ProjectEditView project = projects.create(new CreateProjectCommand("Topic Host", "test"));
|
||||||
|
UUID first = insertTopic("주제 하나", "topic-one");
|
||||||
|
UUID second = insertTopic("주제 둘", "topic-two");
|
||||||
|
|
||||||
|
ProjectEditView saved =
|
||||||
|
projects.update(updateWithTopics(project, java.util.List.of(second, first))).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(saved.topicIds()).containsExactly(second, first);
|
||||||
|
assertThat(saved.currentObjective()).isEqualTo("지금 목표");
|
||||||
|
assertThat(saved.nextStep()).isEqualTo("다음 작업");
|
||||||
|
|
||||||
|
// 공개 조회가 쓰는 조건 그대로 — ACTIVE 인 주제만, display_order 순서로.
|
||||||
|
assertThat(
|
||||||
|
jdbcClient
|
||||||
|
.sql(
|
||||||
|
"SELECT t.name FROM project_topic pt"
|
||||||
|
+ " JOIN topic t ON t.id = pt.topic_id"
|
||||||
|
+ " WHERE pt.project_id = :id AND t.status = 'ACTIVE'"
|
||||||
|
+ " ORDER BY pt.display_order, t.name")
|
||||||
|
.param("id", project.id())
|
||||||
|
.query(String.class)
|
||||||
|
.list())
|
||||||
|
.containsExactly("주제 둘", "주제 하나");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 부분 수정이 아니라 전체 교체다 — 빈 목록은 "주제를 전부 뗀다" 는 뜻이어야 한다. */
|
||||||
|
@Test
|
||||||
|
void savingAnEmptyTopicListDetachesEveryTopic() {
|
||||||
|
ProjectEditView project = projects.create(new CreateProjectCommand("Detach Host", "test"));
|
||||||
|
UUID topic = insertTopic("뗄 주제", "topic-detach");
|
||||||
|
ProjectEditView withTopic =
|
||||||
|
projects.update(updateWithTopics(project, java.util.List.of(topic))).orElseThrow();
|
||||||
|
assertThat(withTopic.topicIds()).containsExactly(topic);
|
||||||
|
|
||||||
|
ProjectEditView cleared =
|
||||||
|
projects.update(updateWithTopics(withTopic, java.util.List.of())).orElseThrow();
|
||||||
|
|
||||||
|
assertThat(cleared.topicIds()).isEmpty();
|
||||||
|
assertThat(
|
||||||
|
jdbcClient
|
||||||
|
.sql("SELECT count(*) FROM project_topic WHERE project_id = :id")
|
||||||
|
.param("id", project.id())
|
||||||
|
.query(Integer.class)
|
||||||
|
.single())
|
||||||
|
.isZero();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -17,11 +17,13 @@ public record UpdateProjectCommand(
|
|||||||
String currentObjective,
|
String currentObjective,
|
||||||
String nextStep,
|
String nextStep,
|
||||||
List<String> technologyLabels,
|
List<String> technologyLabels,
|
||||||
|
List<UUID> topicIds,
|
||||||
String targetVisibility,
|
String targetVisibility,
|
||||||
Integer featuredOrder,
|
Integer featuredOrder,
|
||||||
String actor) {
|
String actor) {
|
||||||
|
|
||||||
public UpdateProjectCommand {
|
public UpdateProjectCommand {
|
||||||
technologyLabels = technologyLabels == null ? List.of() : List.copyOf(technologyLabels);
|
technologyLabels = technologyLabels == null ? List.of() : List.copyOf(technologyLabels);
|
||||||
|
topicIds = topicIds == null ? List.of() : List.copyOf(topicIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-2
@@ -7,8 +7,10 @@ import java.util.UUID;
|
|||||||
/**
|
/**
|
||||||
* 계약 {@code ProjectEditResponse}.
|
* 계약 {@code ProjectEditResponse}.
|
||||||
*
|
*
|
||||||
* <p>{@code topicIds}/{@code documentLinks}/{@code questionLinks} 는 링크 테이블이 소유한다. (가) 범위에서는 그 편집
|
* <p>{@code topicIds} 는 {@code project_topic} 이 소유한다. 프로젝트 편집 화면이 이 목록을 고르고, 공개 프로젝트 화면의 "주요 주제" 가
|
||||||
* 화면이 없으므로 항상 비어 있고, 링크를 다루는 화면이 생길 때 같은 뷰에 채운다.
|
* 그 결과를 그린다 — 그 화면은 처음부터 주제를 읽고 있었지만 저장할 경로가 없어 늘 비어 있었다.
|
||||||
|
*
|
||||||
|
* <p>{@code documentLinks}/{@code questionLinks} 는 아직 편집 화면이 없어 항상 비어 있다.
|
||||||
*/
|
*/
|
||||||
public record ProjectEditView(
|
public record ProjectEditView(
|
||||||
UUID id,
|
UUID id,
|
||||||
@@ -23,6 +25,7 @@ public record ProjectEditView(
|
|||||||
String currentObjective,
|
String currentObjective,
|
||||||
String nextStep,
|
String nextStep,
|
||||||
List<String> technologyLabels,
|
List<String> technologyLabels,
|
||||||
|
List<UUID> topicIds,
|
||||||
String workflowStatus,
|
String workflowStatus,
|
||||||
String targetVisibility,
|
String targetVisibility,
|
||||||
Integer featuredOrder,
|
Integer featuredOrder,
|
||||||
@@ -32,5 +35,6 @@ public record ProjectEditView(
|
|||||||
|
|
||||||
public ProjectEditView {
|
public ProjectEditView {
|
||||||
technologyLabels = technologyLabels == null ? List.of() : List.copyOf(technologyLabels);
|
technologyLabels = technologyLabels == null ? List.of() : List.copyOf(technologyLabels);
|
||||||
|
topicIds = topicIds == null ? List.of() : List.copyOf(topicIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -15,9 +15,11 @@ public record PublishedProjectView(
|
|||||||
String nextStep,
|
String nextStep,
|
||||||
String systemOverviewMarkdown,
|
String systemOverviewMarkdown,
|
||||||
List<String> technologies,
|
List<String> technologies,
|
||||||
|
List<TopicSummaryView> topics,
|
||||||
Instant updatedAt) {
|
Instant updatedAt) {
|
||||||
|
|
||||||
public PublishedProjectView {
|
public PublishedProjectView {
|
||||||
technologies = technologies == null ? List.of() : List.copyOf(technologies);
|
technologies = technologies == null ? List.of() : List.copyOf(technologies);
|
||||||
|
topics = topics == null ? List.of() : List.copyOf(topics);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# source: tech-log-design-package contracts/openapi/studio-v1.yaml @ 436937f (master)
|
# source: tech-log-design-package contracts/openapi/studio-v1.yaml @ 06ae075 (master)
|
||||||
6fc015ca6727af88b7fb0088e02ba97846e1dd79fb0d4fc593cc79f2a3b9795f studio-v1.yaml
|
6fc015ca6727af88b7fb0088e02ba97846e1dd79fb0d4fc593cc79f2a3b9795f studio-v1.yaml
|
||||||
# source: tech-log-design-package contracts/openapi/public-v1.yaml @ 436937f (master)
|
# source: tech-log-design-package contracts/openapi/public-v1.yaml @ 06ae075 (master)
|
||||||
37e6f804165de3e492e975075bea563ee41ae74076222a3562d3452631bfdb2b public-v1.yaml
|
702d6666a8feba9899c7eb7c2a94a0880bcb23b178c7ed2009c6e69d9a1c848c public-v1.yaml
|
||||||
# source: tech-log-design-package contracts/openapi/studio-management-v1.yaml @ 436937f (master)
|
# source: tech-log-design-package contracts/openapi/studio-management-v1.yaml @ 06ae075 (master)
|
||||||
72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55 studio-management-v1.yaml
|
72650735061fde627f5037571eb986cb758f44a546f065c88408399f8eec4a55 studio-management-v1.yaml
|
||||||
|
|||||||
@@ -1572,6 +1572,13 @@ components:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
topics:
|
||||||
|
type: array
|
||||||
|
description: |-
|
||||||
|
이 프로젝트가 다루는 주제. 프로젝트 화면의 "주요 주제" 가 이 목록을 그린다.
|
||||||
|
화면은 처음부터 이 값을 읽고 있었지만 계약에 자리가 없어 늘 비어 있었다.
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/TopicSummary'
|
||||||
updatedAt: *id003
|
updatedAt: *id003
|
||||||
featuredDecision:
|
featuredDecision:
|
||||||
$ref: '#/components/schemas/RelatedEntry'
|
$ref: '#/components/schemas/RelatedEntry'
|
||||||
|
|||||||
Reference in New Issue
Block a user