Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.wooteco.wiki.graph.dto.CrewGraphResponse;
import com.wooteco.wiki.graph.service.CrewGraphQueryService;
import io.swagger.v3.oas.annotations.Operation;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -19,12 +20,19 @@ public class CrewGraphController {

private final CrewGraphQueryService crewGraphQueryService;

@Operation(summary = "크루 관계 그래프 조회", description = "기수에 속한 크루 문서 노드와 관계를 조회합니다.")
@Operation(summary = "크루 관계 그래프 조회", description = "기수에 속한 크루 문서 관계를 조회하고, 조직 선택 시 조직 노드와 연결 간선을 추가합니다.")
@GetMapping
public ApiResponse<SuccessBody<CrewGraphResponse>> findByGeneration(
@RequestParam String generation
@RequestParam String generation,
@RequestParam(
name = "organizationDocumentUuid",
required = false
) UUID selectedOrganizationDocumentUuid
) {
CrewGraphResponse response = crewGraphQueryService.findByGeneration(generation);
CrewGraphResponse response = crewGraphQueryService.findByGeneration(
generation,
selectedOrganizationDocumentUuid
);
return ApiResponseGenerator.success(response);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wooteco.wiki.graph.dto;

public enum GraphEdgeType {
REFERENCE
REFERENCE,
ORGANIZATION_LINK
}
11 changes: 11 additions & 0 deletions src/main/java/com/wooteco/wiki/graph/dto/GraphNodeResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ public static GraphNodeResponse fromCrewDocument(
GraphNodeType.CREW
);
}

public static GraphNodeResponse fromOrganizationDocument(
UUID documentUuid,
String title
) {
return new GraphNodeResponse(
documentUuid,
title,
GraphNodeType.ORGANIZATION
);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/wooteco/wiki/graph/dto/GraphNodeType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wooteco.wiki.graph.dto;

public enum GraphNodeType {
CREW
CREW,
ORGANIZATION
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.wooteco.wiki.graph.repository;

import java.util.List;
import java.util.UUID;

public interface CrewGraphQueryRepository {

List<CrewGraphReadModel> findAllCrewDocumentsByGenerationTitle(String generationTitle);

List<UUID> findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid(
String generationTitle,
UUID organizationDocumentUuid
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
import com.wooteco.wiki.graph.dto.GraphNodeResponse;
import com.wooteco.wiki.graph.repository.CrewGraphQueryRepository;
import com.wooteco.wiki.graph.repository.CrewGraphReadModel;
import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument;
import com.wooteco.wiki.organizationdocument.repository.OrganizationDocumentRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,14 +26,29 @@
public class CrewGraphQueryService {

private final CrewGraphQueryRepository crewGraphQueryRepository;
private final OrganizationDocumentRepository organizationDocumentRepository;
private final CrewDocumentReferenceExtractor crewDocumentReferenceExtractor;

@Transactional(readOnly = true)
public CrewGraphResponse findByGeneration(String generation) {
return findByGeneration(generation, null);
}

@Transactional(readOnly = true)
public CrewGraphResponse findByGeneration(
String generation,
UUID organizationDocumentUuid
) {
validateGeneration(generation);
List<CrewGraphReadModel> readModels = crewGraphQueryRepository.findAllCrewDocumentsByGenerationTitle(generation);
List<GraphNodeResponse> nodes = createNodes(readModels);
List<GraphEdgeResponse> edges = createEdges(readModels);
List<GraphNodeResponse> nodes = new ArrayList<>(createCrewNodes(readModels));
List<GraphEdgeResponse> edges = new ArrayList<>(createReferenceEdges(readModels));
addOrganizationGraphIfSelected(
generation,
organizationDocumentUuid,
nodes,
edges
);
return CrewGraphResponse.of(nodes, edges);
}

Expand All @@ -40,7 +58,7 @@ private void validateGeneration(String generation) {
}
}

private List<GraphNodeResponse> createNodes(List<CrewGraphReadModel> readModels) {
private List<GraphNodeResponse> createCrewNodes(List<CrewGraphReadModel> readModels) {
List<GraphNodeResponse> nodes = new ArrayList<>();
for (CrewGraphReadModel readModel : readModels) {
GraphNodeResponse node = GraphNodeResponse.fromCrewDocument(
Expand All @@ -52,8 +70,8 @@ private List<GraphNodeResponse> createNodes(List<CrewGraphReadModel> readModels)
return List.copyOf(nodes);
}

private List<GraphEdgeResponse> createEdges(List<CrewGraphReadModel> readModels) {
Set<UUID> nodeDocumentUuids = createNodeDocumentUuids(readModels);
private List<GraphEdgeResponse> createReferenceEdges(List<CrewGraphReadModel> readModels) {
Set<UUID> nodeDocumentUuids = createCrewDocumentUuids(readModels);
Set<GraphEdgeResponse> edges = new HashSet<>();
for (CrewGraphReadModel readModel : readModels) {
addReferenceEdges(readModel, nodeDocumentUuids, edges);
Expand All @@ -65,7 +83,7 @@ private List<GraphEdgeResponse> createEdges(List<CrewGraphReadModel> readModels)
return List.copyOf(sortedEdges);
}

private Set<UUID> createNodeDocumentUuids(List<CrewGraphReadModel> readModels) {
private Set<UUID> createCrewDocumentUuids(List<CrewGraphReadModel> readModels) {
Set<UUID> documentUuids = new HashSet<>();
for (CrewGraphReadModel readModel : readModels) {
documentUuids.add(readModel.documentUuid());
Expand Down Expand Up @@ -122,4 +140,66 @@ private GraphEdgeResponse createReferenceEdge(
GraphEdgeType.REFERENCE
);
}

private void addOrganizationGraphIfSelected(
String generation,
UUID organizationDocumentUuid,
List<GraphNodeResponse> nodes,
List<GraphEdgeResponse> edges
) {
if (organizationDocumentUuid == null) {
return;
}
OrganizationDocument organizationDocument = findOrganizationDocument(organizationDocumentUuid);
validateOrganizationIsNotGeneration(generation, organizationDocument);
if (nodes.isEmpty()) {
return;
}
nodes.add(GraphNodeResponse.fromOrganizationDocument(
organizationDocument.getUuid(),
organizationDocument.getTitle()
));
List<UUID> linkedCrewDocumentUuids = crewGraphQueryRepository
.findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid(
generation,
organizationDocumentUuid
);
addOrganizationLinkEdges(
organizationDocumentUuid,
linkedCrewDocumentUuids,
edges
);
}

private OrganizationDocument findOrganizationDocument(UUID organizationDocumentUuid) {
Optional<OrganizationDocument> organizationDocument = organizationDocumentRepository.findByUuid(
organizationDocumentUuid
);
return organizationDocument.orElseThrow(
() -> new WikiException(ErrorCode.ORGANIZATION_DOCUMENT_NOT_FOUND)
);
}

private void validateOrganizationIsNotGeneration(
String generation,
OrganizationDocument organizationDocument
) {
if (generation.equals(organizationDocument.getTitle())) {
throw new WikiException(ErrorCode.VALIDATION_ERROR);
}
}

private void addOrganizationLinkEdges(
UUID organizationDocumentUuid,
List<UUID> linkedCrewDocumentUuids,
List<GraphEdgeResponse> edges
) {
for (UUID crewDocumentUuid : linkedCrewDocumentUuids) {
edges.add(new GraphEdgeResponse(
organizationDocumentUuid,
crewDocumentUuid,
GraphEdgeType.ORGANIZATION_LINK
));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -72,5 +73,22 @@ List<CrewGraphReadModel> findAllCrewDocumentsByGenerationTitle(
@Param("generationTitle") String generationTitle
);

@Override
@Query("""
SELECT selectedOrganizationLink.crewDocument.uuid
FROM DocumentOrganizationLink selectedOrganizationLink
WHERE selectedOrganizationLink.organizationDocument.uuid = :organizationDocumentUuid
AND selectedOrganizationLink.crewDocument IN (
SELECT generationLink.crewDocument
FROM DocumentOrganizationLink generationLink
WHERE generationLink.organizationDocument.title = :generationTitle
)
ORDER BY selectedOrganizationLink.crewDocument.uuid
""")
List<UUID> findAllCrewDocumentUuidsByGenerationTitleAndOrganizationDocumentUuid(
@Param("generationTitle") String generationTitle,
@Param("organizationDocumentUuid") UUID organizationDocumentUuid
);

void deleteAllByCrewDocument(CrewDocument crewDocument);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,90 @@ void findByGeneration_success_byMatchingGenerationTitle() {
.body("data.edges[0].type", equalTo("REFERENCE"));
}

@Test
@DisplayName("선택한 조직 노드와 현재 기수에서 조직에 연결된 크루 간선을 반환한다.")
void findByGeneration_success_bySelectedOrganization() {
// given
UUID firstCrewUuid = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID secondCrewUuid = UUID.fromString("22222222-2222-2222-2222-222222222222");
UUID organizationDocumentUuid = UUID.fromString("33333333-3333-3333-3333-333333333333");
CrewDocument firstCrew = saveCrewDocument(
"가람(8기)",
"https://crew-wiki.site/wiki/22222222-2222-2222-2222-222222222222",
firstCrewUuid
);
CrewDocument secondCrew = saveCrewDocument(
"나래(8기)",
"contents",
secondCrewUuid
);
OrganizationDocument generation = saveOrganizationDocument("8기");
OrganizationDocument backend = saveOrganizationDocument(
"백엔드",
organizationDocumentUuid
);
saveLink(firstCrew, generation);
saveLink(firstCrew, backend);
saveLink(secondCrew, generation);

// when & then
RestAssured.given().log().all()
.queryParam("generation", "8기")
.queryParam("organizationDocumentUuid", organizationDocumentUuid)
.when()
.get("/graph")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.body("data.nodes", hasSize(3))
.body("data.nodes[0].documentUuid", equalTo(firstCrewUuid.toString()))
.body("data.nodes[0].type", equalTo("CREW"))
.body("data.nodes[1].documentUuid", equalTo(secondCrewUuid.toString()))
.body("data.nodes[1].type", equalTo("CREW"))
.body("data.nodes[2].documentUuid", equalTo(organizationDocumentUuid.toString()))
.body("data.nodes[2].title", equalTo("백엔드"))
.body("data.nodes[2].type", equalTo("ORGANIZATION"))
.body("data.edges", hasSize(2))
.body("data.edges[0].sourceDocumentUuid", equalTo(firstCrewUuid.toString()))
.body("data.edges[0].targetDocumentUuid", equalTo(secondCrewUuid.toString()))
.body("data.edges[0].type", equalTo("REFERENCE"))
.body("data.edges[1].sourceDocumentUuid", equalTo(organizationDocumentUuid.toString()))
.body("data.edges[1].targetDocumentUuid", equalTo(firstCrewUuid.toString()))
.body("data.edges[1].type", equalTo("ORGANIZATION_LINK"));
}

@Test
@DisplayName("선택한 조직 문서가 없으면 조회 실패를 반환한다.")
void findByGeneration_fail_byMissingOrganizationDocument() {
// given
OrganizationDocument generation = saveOrganizationDocument("8기");
CrewDocument crewDocument = saveCrewDocument("가람(8기)");
saveLink(crewDocument, generation);

// when & then
RestAssured.given().log().all()
.queryParam("generation", "8기")
.queryParam("organizationDocumentUuid", UUID.randomUUID())
.when()
.get("/graph")
.then().log().all()
.statusCode(HttpStatus.NOT_FOUND.value())
.body("code", equalTo("ORGANIZATION_DOCUMENT_NOT_FOUND"));
}

@Test
@DisplayName("선택한 조직 문서 UUID 형식이 잘못되면 검증 실패를 반환한다.")
void findByGeneration_fail_byInvalidOrganizationDocumentUuid() {
// when & then
RestAssured.given().log().all()
.queryParam("generation", "8기")
.queryParam("organizationDocumentUuid", "invalid-uuid")
.when()
.get("/graph")
.then().log().all()
.statusCode(HttpStatus.BAD_REQUEST.value())
.body("code", equalTo("VALIDATION_ERROR"));
}

@Test
@DisplayName("입력한 조직 제목과 정확히 일치하는 기수가 없으면 빈 그래프를 반환한다.")
void findByGeneration_success_byNoExactGenerationTitle() {
Expand Down Expand Up @@ -152,12 +236,19 @@ private CrewDocument saveCrewDocument(
}

private OrganizationDocument saveOrganizationDocument(String title) {
return saveOrganizationDocument(title, UUID.randomUUID());
}

private OrganizationDocument saveOrganizationDocument(
String title,
UUID uuid
) {
OrganizationDocument organizationDocument = OrganizationDocumentFixture.create(
title,
"contents",
"writer",
10L,
UUID.randomUUID()
uuid
);
return organizationDocumentRepository.save(organizationDocument);
}
Expand Down
Loading
Loading