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
@@ -0,0 +1,30 @@
package com.wooteco.wiki.graph.controller;

import com.wooteco.wiki.global.common.ApiResponse;
import com.wooteco.wiki.global.common.ApiResponse.SuccessBody;
import com.wooteco.wiki.global.common.ApiResponseGenerator;
import com.wooteco.wiki.graph.dto.CrewGraphResponse;
import com.wooteco.wiki.graph.service.CrewGraphQueryService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/graph")
public class CrewGraphController {

private final CrewGraphQueryService crewGraphQueryService;

@Operation(summary = "크루 관계 그래프 조회", description = "기수에 속한 크루 문서 노드와 관계를 조회합니다.")
@GetMapping
public ApiResponse<SuccessBody<CrewGraphResponse>> findByGeneration(
@RequestParam String generation
) {
CrewGraphResponse response = crewGraphQueryService.findByGeneration(generation);
return ApiResponseGenerator.success(response);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/wooteco/wiki/graph/dto/CrewGraphResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wooteco.wiki.graph.dto;

import java.util.List;

public record CrewGraphResponse(
List<GraphNodeResponse> nodes,
List<GraphEdgeResponse> edges
) {

public CrewGraphResponse {
nodes = List.copyOf(nodes);
edges = List.copyOf(edges);
}

public static CrewGraphResponse of(
List<GraphNodeResponse> nodes,
List<GraphEdgeResponse> edges
) {
return new CrewGraphResponse(nodes, edges);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.wooteco.wiki.graph.dto;

import java.util.UUID;

public record GraphEdgeResponse(
UUID sourceDocumentUuid,
UUID targetDocumentUuid,
GraphEdgeType type
) {
}
5 changes: 5 additions & 0 deletions src/main/java/com/wooteco/wiki/graph/dto/GraphEdgeType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.wooteco.wiki.graph.dto;

public enum GraphEdgeType {
REFERENCE
}
21 changes: 21 additions & 0 deletions src/main/java/com/wooteco/wiki/graph/dto/GraphNodeResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wooteco.wiki.graph.dto;

import java.util.UUID;

public record GraphNodeResponse(
UUID documentUuid,
String title,
GraphNodeType type
) {

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

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

import java.util.List;

public interface CrewGraphQueryRepository {

List<CrewGraphReadModel> findAllCrewDocumentsByGenerationTitle(String generationTitle);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.wooteco.wiki.graph.repository;

import java.util.UUID;

public record CrewGraphReadModel(
UUID documentUuid,
String title,
String contents
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.wooteco.wiki.graph.service;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;

@Component
public class CrewDocumentReferenceExtractor {

private static final String UUID_PATTERN =
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
private static final Pattern INTERNAL_DOCUMENT_LINK_PATTERN = Pattern.compile(
"(?<![A-Za-z0-9._~:/?#@!$&'*+,;=%-])"
+ "https://crew-wiki\\.site/wiki/(?<uuid>" + UUID_PATTERN + ")"
+ "(?=$|[^A-Za-z0-9._~-])"
);

public List<UUID> extract(String contents) {
if (contents == null || contents.isBlank()) {
return List.of();
}
Set<UUID> references = new LinkedHashSet<>();
Matcher matcher = INTERNAL_DOCUMENT_LINK_PATTERN.matcher(contents);
while (matcher.find()) {
addReferenceIfValid(contents, matcher, references);
}
return List.copyOf(references);
}

private void addReferenceIfValid(
String contents,
Matcher matcher,
Set<UUID> references
) {
if (isMarkdownImage(contents, matcher.start())) {
return;
}
references.add(UUID.fromString(matcher.group("uuid")));
}

private boolean isMarkdownImage(
String contents,
int linkStart
) {
boolean startsAfterMarkdownLabel = linkStart >= 3
&& contents.charAt(linkStart - 1) == '('
&& contents.charAt(linkStart - 2) == ']';
if (!startsAfterMarkdownLabel) {
return false;
}
int altTextStart = contents.lastIndexOf('[', linkStart - 2);
return altTextStart > 0 && contents.charAt(altTextStart - 1) == '!';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.wooteco.wiki.graph.service;

import com.wooteco.wiki.global.exception.ErrorCode;
import com.wooteco.wiki.global.exception.WikiException;
import com.wooteco.wiki.graph.dto.CrewGraphResponse;
import com.wooteco.wiki.graph.dto.GraphEdgeResponse;
import com.wooteco.wiki.graph.dto.GraphEdgeType;
import com.wooteco.wiki.graph.dto.GraphNodeResponse;
import com.wooteco.wiki.graph.repository.CrewGraphQueryRepository;
import com.wooteco.wiki.graph.repository.CrewGraphReadModel;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class CrewGraphQueryService {

private final CrewGraphQueryRepository crewGraphQueryRepository;
private final CrewDocumentReferenceExtractor crewDocumentReferenceExtractor;

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

private void validateGeneration(String generation) {
if (generation == null || generation.isBlank()) {
throw new WikiException(ErrorCode.VALIDATION_ERROR);
}
}

private List<GraphNodeResponse> createNodes(List<CrewGraphReadModel> readModels) {
List<GraphNodeResponse> nodes = new ArrayList<>();
for (CrewGraphReadModel readModel : readModels) {
GraphNodeResponse node = GraphNodeResponse.fromCrewDocument(
readModel.documentUuid(),
readModel.title()
);
nodes.add(node);
}
return List.copyOf(nodes);
}

private List<GraphEdgeResponse> createEdges(List<CrewGraphReadModel> readModels) {
Set<UUID> nodeDocumentUuids = createNodeDocumentUuids(readModels);
Set<GraphEdgeResponse> edges = new HashSet<>();
for (CrewGraphReadModel readModel : readModels) {
addReferenceEdges(readModel, nodeDocumentUuids, edges);
}
List<GraphEdgeResponse> sortedEdges = new ArrayList<>(edges);
sortedEdges.sort(Comparator
.comparing(GraphEdgeResponse::sourceDocumentUuid)
.thenComparing(GraphEdgeResponse::targetDocumentUuid));
return List.copyOf(sortedEdges);
}

private Set<UUID> createNodeDocumentUuids(List<CrewGraphReadModel> readModels) {
Set<UUID> documentUuids = new HashSet<>();
for (CrewGraphReadModel readModel : readModels) {
documentUuids.add(readModel.documentUuid());
}
return documentUuids;
}

private void addReferenceEdges(
CrewGraphReadModel sourceDocument,
Set<UUID> nodeDocumentUuids,
Set<GraphEdgeResponse> edges
) {
List<UUID> referencedDocumentUuids = crewDocumentReferenceExtractor.extract(sourceDocument.contents());
for (UUID targetDocumentUuid : referencedDocumentUuids) {
addReferenceEdgeIfValid(
sourceDocument.documentUuid(),
targetDocumentUuid,
nodeDocumentUuids,
edges
);
}
}

private void addReferenceEdgeIfValid(
UUID sourceDocumentUuid,
UUID targetDocumentUuid,
Set<UUID> nodeDocumentUuids,
Set<GraphEdgeResponse> edges
) {
if (sourceDocumentUuid.equals(targetDocumentUuid)) {
return;
}
if (!nodeDocumentUuids.contains(targetDocumentUuid)) {
return;
}
GraphEdgeResponse edge = createReferenceEdge(sourceDocumentUuid, targetDocumentUuid);
edges.add(edge);
}

private GraphEdgeResponse createReferenceEdge(
UUID sourceDocumentUuid,
UUID targetDocumentUuid
) {
if (sourceDocumentUuid.compareTo(targetDocumentUuid) < 0) {
return new GraphEdgeResponse(
sourceDocumentUuid,
targetDocumentUuid,
GraphEdgeType.REFERENCE
);
}
return new GraphEdgeResponse(
targetDocumentUuid,
sourceDocumentUuid,
GraphEdgeType.REFERENCE
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.wooteco.wiki.document.domain.CrewDocument;
import com.wooteco.wiki.document.repository.GenerationCrewOrganizationReadModel;
import com.wooteco.wiki.document.repository.GenerationCrewQueryRepository;
import com.wooteco.wiki.graph.repository.CrewGraphQueryRepository;
import com.wooteco.wiki.graph.repository.CrewGraphReadModel;
import com.wooteco.wiki.organizationdocument.domain.DocumentOrganizationLink;
import com.wooteco.wiki.organizationdocument.domain.OrganizationDocument;
import java.util.List;
Expand All @@ -13,7 +15,8 @@

public interface DocumentOrganizationLinkRepository extends
JpaRepository<DocumentOrganizationLink, Long>,
GenerationCrewQueryRepository {
GenerationCrewQueryRepository,
CrewGraphQueryRepository {

Optional<DocumentOrganizationLink> findByCrewDocumentAndOrganizationDocument(
CrewDocument crewDocument,
Expand Down Expand Up @@ -53,5 +56,21 @@ List<GenerationCrewOrganizationReadModel> findAllByGenerationTitle(
@Param("generationTitle") String generationTitle
);

@Override
@Query("""
SELECT new com.wooteco.wiki.graph.repository.CrewGraphReadModel(
crewDocument.uuid,
crewDocument.title,
crewDocument.contents
)
FROM DocumentOrganizationLink documentOrganizationLink
JOIN documentOrganizationLink.crewDocument crewDocument
WHERE documentOrganizationLink.organizationDocument.title = :generationTitle
ORDER BY crewDocument.title
""")
List<CrewGraphReadModel> findAllCrewDocumentsByGenerationTitle(
@Param("generationTitle") String generationTitle
);

void deleteAllByCrewDocument(CrewDocument crewDocument);
}
Loading
Loading