Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public final class ExampleKeyConstant {
public static final String DRUG_GET_BY_ARGOT_200 = "Drug__Get__By__Argot__200";
public static final String DRUG_GET_BY_ID_404 = "Drug__Get__By__Id__404";
public static final String DRUG_GRAPH_FIND_ALL_200 = "Drug__Graph__Find__All__200";
public static final String DRUG_GET_TRACE_200 = "Drug__Get__Trace__200";
public static final String DRUG_GET_TRACE_404 = "Drug__Get__Trace__404";

// Argot
public static final String ARGOT_FIND_ALL_200 = "Argot__Find__All__200";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
package com.vigilante.retriever.infrastructure.config;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

import org.neo4j.driver.Driver;
import org.neo4j.driver.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.lang.Nullable;

@Configuration
@EnableNeo4jRepositories(basePackages = {"com.vigilante.retriever.v1.*.adapter.out.persistence.neo4j.repository"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,10 @@ public ArgotTraceResponse toTraceResponse(ArgotTraceVO vo) {
.refersToDrugs(vo.refersToDrugs())
.build();
}

public Set<ArgotTraceResponse> toTraceResponseSet(Set<ArgotGraphView> GraphViews) {
return GraphViews.stream()
.map(graphView -> toTraceResponse(ArgotTraceVO.create(graphView)))
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.vigilante.retriever.adapter.web.dto.response.CommonResponse;
import com.vigilante.retriever.adapter.web.openapi.annotation.ApiErrorExample;
import com.vigilante.retriever.adapter.web.openapi.annotation.ApiSuccessExample;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugGraphInfoResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugTraceResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -25,4 +27,13 @@ public interface DrugGraphApi {
@ApiSuccessExample({@ApiSuccessExample.Success(code = "200", exampleKey = DRUG_GRAPH_FIND_ALL_200)})
@ApiErrorExample(include = {"401", "500"})
ResponseEntity<CommonResponse<List<DrugGraphInfoResponse>>> findAll();

@GetMapping("/{drugBankId}")
@Operation(summary = "마약 그래프 상세 추적 조회", description = "지정한 마약에 대해 참조하는 은어와 판매 채널 정보를 포함한 추적 정보를 조회합니다.")
@ApiSuccessExample({@ApiSuccessExample.Success(code = "200", exampleKey = DRUG_GET_TRACE_200)})
@ApiErrorExample(
include = {"401", "500"},
custom = @ApiErrorExample.ErrorSpec(code = "404", exampleKey = DRUG_GET_TRACE_404)
)
ResponseEntity<DrugTraceResponse> getDrugTrace(@PathVariable String drugBankId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.vigilante.retriever.adapter.web.dto.response.CommonResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.DrugGraphApi;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugGraphInfoResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugTraceResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.mapper.DrugWebMapper;
import com.vigilante.retriever.v1.drug.domain.port.in.GetDrugGraphUseCase;

Expand All @@ -26,4 +27,13 @@ public ResponseEntity<CommonResponse<List<DrugGraphInfoResponse>>> findAll() {

return CommonResponse.retrieved(response);
}

@Override
public ResponseEntity<DrugTraceResponse> getDrugTrace(String drugBankId) {
DrugTraceResponse response = drugWebMapper.toTraceResponse(
getDrugGraphUseCase.findDrugWithAllRelationships(drugBankId)
);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.vigilante.retriever.v1.drug.adapter.in.web.dto.response;

import java.util.Set;

import com.vigilante.retriever.v1.argot.adapter.in.web.dto.response.ArgotTraceResponse;

import lombok.Builder;

@Builder
public record DrugTraceResponse(
String drugBankId,
String name,
String englishName,
String drugType,
Set<ArgotTraceResponse> referredByArgots
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

import java.util.List;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;

import com.vigilante.retriever.v1.argot.adapter.in.web.mapper.ArgotWebMapper;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugGraphInfoResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugInfoResponse;
import com.vigilante.retriever.v1.drug.adapter.in.web.dto.response.DrugTraceResponse;
import com.vigilante.retriever.v1.drug.domain.entity.DrugEntity;
import com.vigilante.retriever.v1.drug.domain.graphview.DrugGraphView;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class DrugWebMapper {

private final ObjectProvider<ArgotWebMapper> argotWebMapperProvider;

public DrugInfoResponse toResponse(DrugEntity entity) {
return DrugInfoResponse.builder()
.id(entity.id())
Expand Down Expand Up @@ -56,4 +64,15 @@ public List<DrugGraphInfoResponse> toGraphResponseList(List<DrugGraphView> graph
.map(this::toGraphResponse)
.toList();
}

public DrugTraceResponse toTraceResponse(DrugGraphView graphView) {
ArgotWebMapper argotWebMapper = argotWebMapperProvider.getObject();
return DrugTraceResponse.builder()
.drugBankId(graphView.drugBankId())
.name(graphView.name())
.englishName(graphView.englishName())
.drugType(graphView.drugType())
.referredByArgots(argotWebMapper.toTraceResponseSet(graphView.referredByArgots()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.vigilante.retriever.v1.drug.adapter.out.mapper;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.ReportingPolicy;
import org.springframework.context.annotation.Primary;

import com.vigilante.retriever.infrastructure.common.mapper.GenericNeo4jMapper;
import com.vigilante.retriever.v1.argot.domain.graphview.ArgotGraphView;
import com.vigilante.retriever.v1.channel.domain.graphview.ChannelGraphView;
import com.vigilante.retriever.v1.drug.adapter.out.persistence.neo4j.node.DrugNode;
import com.vigilante.retriever.v1.drug.domain.graphview.DrugGraphView;

Expand All @@ -22,8 +30,100 @@ public interface DrugNeo4jMapper extends GenericNeo4jMapper<DrugNode, DrugGraphV
DrugNode toNode(DrugGraphView graphView);

@Override
@Mapping(target = "referredByArgots", ignore = true)
DrugGraphView toGraphView(DrugNode document);

// Drug의 referredByArgots를 1 depth만 매핑하여 순환 참조 방지
@AfterMapping
default void mapRelationshipsShallow(
@MappingTarget DrugGraphView.DrugGraphViewBuilder builder,
DrugNode node) {
// referredByArgots 매핑 (shallow - refersDrugs와 soldByChannels를 1 depth만 보여주고 내부는 빈 Set)
if (node.getReferredByArgots() != null && !node.getReferredByArgots().isEmpty()) {
Set<ArgotGraphView> shallowReferredByArgots =
node.getReferredByArgots()
.stream()
.map(argot -> {
// refersDrugs를 1 depth만 매핑
Set<DrugGraphView> drugViews =
(argot.getRefersDrugs() != null && !argot.getRefersDrugs().isEmpty()) ?
argot.getRefersDrugs()
.stream()
.map(drug -> DrugGraphView.builder()
.drugBankId(drug.getDrugBankId())
.name(drug.getName())
.englishName(drug.getEnglishName())
.drugType(drug.getDrugType())
.referredByArgots(Collections.emptySet()) // 2 depth는 빈 Set
.build())
.collect(Collectors.toSet()) : Collections.emptySet();

// soldByChannels를 1 depth만 매핑
Set<ChannelGraphView> shallowChannels =
(argot.getSoldByChannels() != null && !argot.getSoldByChannels().isEmpty()) ?
argot.getSoldByChannels()
.stream()
.map(channel -> {
// sellsArgots를 1 depth 표시 (내부 관계는 빈 Set)
Set<ArgotGraphView> channelArgots =
(channel.getSellsArgots() != null && !channel.getSellsArgots().isEmpty()) ?
channel.getSellsArgots()
.stream()
.map(a -> ArgotGraphView.builder()
.name(a.getName())
.description(a.getDescription())
.refersDrugs(Collections.emptySet()) // 2 depth는 빈 Set
.soldByChannels(Collections.emptySet()) // 2 depth는 빈 Set
.build())
.collect(Collectors.toSet()) : Collections.emptySet();

// promotedByPosts를 1 depth 표시 (내부 관계는 빈 Set)
Set<com.vigilante.retriever.v1.post.domain.graphview.PostGraphView> channelPosts =
(channel.getPromotedByPosts() != null && !channel.getPromotedByPosts()
.isEmpty()) ?
channel.getPromotedByPosts()
.stream()
.map(
post -> com.vigilante.retriever.v1.post.domain.graphview.PostGraphView.builder()
.postId(post.getPostId())
.title(post.getTitle())
.link(post.getLink())
.domain(post.getDomain())
.content(post.getContent())
.cluster(post.getCluster())
.discoveredAt(post.getDiscoveredAt())
.updatedAt(post.getUpdatedAt())
.isDeleted(post.isDeleted())
.promotesChannels(Collections.emptySet()) // 2 depth는 빈 Set
.similarPosts(Collections.emptySet()) // 2 depth는 빈 Set
.build())
.collect(Collectors.toSet()) : Collections.emptySet();

return ChannelGraphView.builder()
.channelId(channel.getChannelId())
.title(channel.getTitle())
.username(channel.getUsername())
.status(channel.getStatus())
.sellsArgots(channelArgots) // 1 depth 표시
.promotedByPosts(channelPosts) // 1 depth 표시
.build();
})
.collect(Collectors.toSet()) : Collections.emptySet();

return ArgotGraphView.builder()
.name(argot.getName())
.description(argot.getDescription())
.refersDrugs(drugViews) // 1 depth 표시
.soldByChannels(shallowChannels) // 1 depth 표시
.build();
})
.collect(Collectors.toSet());
builder.referredByArgots(shallowReferredByArgots);
} else {
builder.referredByArgots(Collections.emptySet());
}
}

@Override
List<DrugNode> getNodeList(List<DrugGraphView> graphViewList);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.vigilante.retriever.v1.drug.adapter.out.persistence.neo4j.adapter;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Component;

Expand All @@ -24,4 +25,9 @@ public List<DrugGraphView> findAll() {
List<DrugNode> allDrug = drugNeo4jRepository.findAll();
return drugGraphMapper.getGraphViewList(allDrug);
}

@Override
public Optional<DrugGraphView> findDrugWithAllRelationships(String drugBankId) {
return drugNeo4jRepository.findDrugWithAllRelationships(drugBankId).map(drugGraphMapper::toGraphView);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.vigilante.retriever.v1.drug.adapter.out.persistence.neo4j.node;

import java.util.HashSet;
import java.util.Set;

import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.vigilante.retriever.v1.argot.adapter.out.persistence.neo4j.node.ArgotNode;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand All @@ -28,4 +35,8 @@ public class DrugNode {

@Property("drug_type")
private String drugType;

@Relationship(type = "REFERS_TO", direction = Relationship.Direction.INCOMING)
@JsonBackReference
private Set<ArgotNode> referredByArgots = new HashSet<>();
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
package com.vigilante.retriever.v1.drug.adapter.out.persistence.neo4j.repository;

import java.util.Optional;

import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import com.vigilante.retriever.v1.drug.adapter.out.persistence.neo4j.node.DrugNode;

@Repository
public interface DrugNeo4jRepository extends Neo4jRepository<DrugNode, String> {

@Query("""
MATCH (d:Drug {drugbank_id: $drugBankId})
OPTIONAL MATCH (a:Argot)-[rf:REFERS_TO]->(d)
OPTIONAL MATCH (c:Channel)-[sell:SELLS]->(a)
OPTIONAL MATCH (p:Post)-[pr:PROMOTES]->(c)
OPTIONAL MATCH (p)-[sim:SIMILAR_TO]->(sp:Post)
RETURN d, collect(a), collect(rf), collect(c), collect(sell), collect(p), collect(pr), collect(sim), collect(sp)
""")
Optional<DrugNode> findDrugWithAllRelationships(@Param("drugBankId") String drugBankId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.List;

import com.vigilante.retriever.common.domain.annotation.QueryService;
import com.vigilante.retriever.common.domain.exception.NotFoundException;
import com.vigilante.retriever.v1.drug.domain.code.DrugErrorCode;
import com.vigilante.retriever.v1.drug.domain.graphview.DrugGraphView;
import com.vigilante.retriever.v1.drug.domain.port.out.DrugNeo4jPort;

Expand All @@ -17,4 +19,9 @@ public class DrugNeo4jQuery {
public List<DrugGraphView> findAll() {
return drugNeo4jPort.findAll();
}

public DrugGraphView findDrugWithAllRelationships(String drugBankId) {
return drugNeo4jPort.findDrugWithAllRelationships(drugBankId)
.orElseThrow(() -> new NotFoundException(DrugErrorCode.DRUG_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ public class GetDrugGraphService implements GetDrugGraphUseCase {
public List<DrugGraphView> findAll() {
return drugNeo4jQuery.findAll();
}

@Override
public DrugGraphView findDrugWithAllRelationships(String drugBankId) {
return drugNeo4jQuery.findDrugWithAllRelationships(drugBankId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@Getter
@AllArgsConstructor
public enum DrugErrorCode implements BaseCode {
DRUG_NOT_FOUND("DRUG-4041", "해당하는 챗봇을 찾을 수 없습니다.");
DRUG_NOT_FOUND("DRUG-4041", "해당하는 마약을 찾을 수 없습니다.");

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.vigilante.retriever.v1.drug.domain.graphview;

import java.util.Set;

import com.vigilante.retriever.v1.argot.domain.graphview.ArgotGraphView;

import lombok.Builder;

@Builder
public record DrugGraphView(
String drugBankId,
String name,
String englishName,
String drugType
String drugType,
Set<ArgotGraphView> referredByArgots
) {

}
Loading
Loading