Skip to content

Commit

Permalink
Feat: 사용자는 도메인 검색 자동 완성을 사용할 수 있다.
Browse files Browse the repository at this point in the history
Feat: 사용자는 도메인 검색 자동 완성을 사용할 수 있다.
  • Loading branch information
hseong3243 authored Mar 27, 2024
2 parents 2ac0c07 + e8136e9 commit c91acf8
Show file tree
Hide file tree
Showing 20 changed files with 622 additions and 10 deletions.
12 changes: 12 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,15 @@ operation::hub-controller-test/find-hub[snippets='http-request,path-parameters']
==== response

operation::hub-controller-test/find-hub[snippets='http-response']

== 도메인

=== 루트 도메인 목록 조회

==== request

operation::domain-controller-test/find-root-domains[snippets='http-request,query-parameters']

==== response

operation::domain-controller-test/find-root-domains[snippets='http-response,response-fields']
93 changes: 93 additions & 0 deletions src/main/java/com/seong/shoutlink/domain/common/Trie.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.seong.shoutlink.domain.common;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Getter;

@Getter
public class Trie {

@Getter
static class Node {

private final char c;
private final Map<Character, Node> children = new HashMap<>();
private boolean isWord;

public Node(char c) {
this.c = c;
}

public void setWord(boolean word) {
isWord = word;
}

public boolean hasChildren(char c) {
return children.containsKey(c);
}

public boolean isWord() {
return isWord;
}

public void addSuggestions(String word, List<String> suggestions, int count) {
children.forEach((character, childNode) -> {
String suggestionsWord = word + character;
if (childNode.isWord()) {
suggestions.add(suggestionsWord);
}
if (suggestions.size() >= count) {
return;
}
childNode.addSuggestions(suggestionsWord, suggestions, count);
});
}
}

private static final int MAX_SUGGESTION = 100;
private static final int MAX_WORD_LENGTH = 35;
private static final int MAX_PREFIX_LENGTH = 30;
public static final int ZERO = 0;

private final Node root = new Node(' ');

public synchronized void insert(String word) {
if(word.length() > MAX_WORD_LENGTH) {
return;
}

Node currentNode = root;
for (char c : word.toCharArray()) {
Map<Character, Node> children = currentNode.getChildren();
currentNode = children.getOrDefault(c, new Node(c));
children.put(c, currentNode);
}
currentNode.setWord(true);
}

public List<String> search(String prefix, int count) {
if(prefix.length() > MAX_PREFIX_LENGTH) {
prefix = prefix.substring(ZERO, MAX_PREFIX_LENGTH);
}
Node currentNode = root;
for (char c : prefix.toCharArray()) {
if (!currentNode.hasChildren(c)) {
return List.of();
}
Map<Character, Node> children = currentNode.getChildren();
currentNode = children.get(c);
}
return findWords(prefix, currentNode, Math.min(MAX_SUGGESTION, count));
}

private List<String> findWords(String word, Node currentNode, int count) {
List<String> suggestions = new ArrayList<>();
if (currentNode.isWord()) {
suggestions.add(word);
}
currentNode.addSuggestions(word, suggestions, count);
return suggestions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.seong.shoutlink.domain.domain.controller;

import com.seong.shoutlink.domain.domain.controller.request.FindRootDomainsRequest;
import com.seong.shoutlink.domain.domain.service.DomainService;
import com.seong.shoutlink.domain.domain.service.request.FindRootDomainsCommand;
import com.seong.shoutlink.domain.domain.service.response.FindRootDomainsResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/domains")
public class DomainController {

private final DomainService domainService;

@GetMapping("/search")
public ResponseEntity<FindRootDomainsResponse> findRootDomains(
@ModelAttribute @Valid FindRootDomainsRequest request) {
FindRootDomainsResponse response = domainService.findRootDomains(
new FindRootDomainsCommand(request.keyword(), request.size()));
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.seong.shoutlink.domain.domain.controller.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.Objects;

public record FindRootDomainsRequest(
@NotNull(message = "검색어는 필수입니다.")
@Size(min = 1, message = "검색어는 1자 이상이어야 합니다.")
String keyword,
Integer size) {

public FindRootDomainsRequest(String keyword, Integer size) {
this.keyword = keyword;
this.size = Objects.isNull(size) ? 10 : size;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.seong.shoutlink.domain.domain.repository;

import java.util.List;

public interface DomainCacheRepository {

List<String> findRootDomains(String prefix, int count);

void insert(String rootDomain);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.seong.shoutlink.domain.domain.repository;

import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface DomainJpaRepository extends JpaRepository<DomainEntity, Long> {

Optional<DomainEntity> findByRootDomain(String rootDomain);

@Query("select d.rootDomain from DomainEntity d")
List<String> findRootDomains();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.seong.shoutlink.domain.domain.repository;

import com.seong.shoutlink.domain.common.Trie;
import java.util.List;
import org.springframework.stereotype.Repository;

@Repository
public class DomainMemoryRepository implements DomainCacheRepository {

private final Trie trie = new Trie();

@Override
public List<String> findRootDomains(String prefix, int count) {
return trie.search(prefix, count);
}

@Override
public void insert(String rootDomain) {
trie.insert(rootDomain);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.domain.service.DomainRepository;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
Expand All @@ -11,6 +12,7 @@
public class DomainRepositoryImpl implements DomainRepository {

private final DomainJpaRepository domainJpaRepository;
private final DomainCacheRepository domainCacheRepository;

@Override
public Optional<Domain> findByRootDomain(String rootDomain) {
Expand All @@ -23,4 +25,15 @@ public Domain save(Domain domain) {
return domainJpaRepository.save(DomainEntity.create(domain))
.toDomain();
}

@Override
public List<String> findRootDomains(String keyword, int size) {
return domainCacheRepository.findRootDomains(keyword, size);
}

@Override
public void synchronizeRootDomains() {
domainJpaRepository.findRootDomains().forEach(domainCacheRepository::insert);

}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.seong.shoutlink.domain.domain.service;

import com.seong.shoutlink.domain.domain.Domain;
import java.util.List;
import java.util.Optional;

public interface DomainRepository {

Optional<Domain> findByRootDomain(String rootDomain);

Domain save(Domain domain);

List<String> findRootDomains(String keyword, int size);

void synchronizeRootDomains();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.seong.shoutlink.domain.domain.service;

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.domain.service.request.FindRootDomainsCommand;
import com.seong.shoutlink.domain.domain.service.request.UpdateDomainCommand;
import com.seong.shoutlink.domain.domain.service.response.FindRootDomainsResponse;
import com.seong.shoutlink.domain.domain.service.response.UpdateDomainResponse;
import com.seong.shoutlink.domain.domain.util.DomainExtractor;
import com.seong.shoutlink.domain.exception.ErrorCode;
import com.seong.shoutlink.domain.exception.ShoutLinkException;
import com.seong.shoutlink.domain.link.Link;
import com.seong.shoutlink.domain.link.service.LinkRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -34,4 +37,10 @@ public UpdateDomainResponse updateDomain(UpdateDomainCommand command) {
linkRepository.updateLinkDomain(link, domain);
return new UpdateDomainResponse(domain.getDomainId());
}

public FindRootDomainsResponse findRootDomains(FindRootDomainsCommand command) {
List<String> rootDomains = domainRepository.findRootDomains(command.keyword(),
command.size());
return FindRootDomainsResponse.from(rootDomains);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.domain.service.request;

public record FindRootDomainsCommand(String keyword, int size) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.seong.shoutlink.domain.domain.service.response;

import java.util.List;

public record FindRootDomainsResponse(List<String> rootDomains) {

public static FindRootDomainsResponse from(List<String> rootDomains) {
return new FindRootDomainsResponse(rootDomains);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.seong.shoutlink.global.config;

import com.seong.shoutlink.domain.domain.service.DomainRepository;
import com.seong.shoutlink.global.scheduler.DomainScheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulerConfig {

@Bean
public DomainScheduler domainScheduler(DomainRepository domainRepository) {
return new DomainScheduler(domainRepository);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.seong.shoutlink.global.scheduler;

import com.seong.shoutlink.domain.domain.service.DomainRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;

@RequiredArgsConstructor
public class DomainScheduler {

private final DomainRepository domainRepository;

@Scheduled(cron = "0 0 * * * *")
public void synchronizeRootDomains() {
domainRepository.synchronizeRootDomains();
}
}
Loading

0 comments on commit c91acf8

Please sign in to comment.