Skip to content

Commit

Permalink
Feat: 사용자가 링크를 등록하면 도메인 정보를 기록한다.
Browse files Browse the repository at this point in the history
Feat: 사용자가 링크를 등록하면 도메인 정보를 기록한다.
  • Loading branch information
hseong3243 authored Mar 25, 2024
2 parents fb458a8 + 6ab33da commit 2ac0c07
Show file tree
Hide file tree
Showing 26 changed files with 632 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,6 @@ Temporary Items
.idea/jarRepositories.xml
.idea/misc.xml
.idea/vcs.xml
.idea/modules/shoutlink.iml
.idea/modules/shoutlink.main.iml
.idea/modules/shoutlink.test.iml
19 changes: 19 additions & 0 deletions src/main/java/com/seong/shoutlink/domain/domain/Domain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.seong.shoutlink.domain.domain;

import lombok.Getter;

@Getter
public class Domain {

private Long domainId;
private String rootDomain;

public Domain(String rootDomain) {
this(null, rootDomain);
}

public Domain(Long domainId, String rootDomain) {
this.domainId = domainId;
this.rootDomain = rootDomain;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.seong.shoutlink.domain.domain.repository;

import com.seong.shoutlink.domain.common.BaseEntity;
import com.seong.shoutlink.domain.domain.Domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "domain")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DomainEntity extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long domainId;
private String rootDomain;

public DomainEntity(String rootDomain) {
this(null, rootDomain);
}

public DomainEntity(Long domainId, String rootDomain) {
this.domainId = domainId;
this.rootDomain = rootDomain;
}

public static DomainEntity create(Domain domain) {
return new DomainEntity(domain.getRootDomain());
}

public Domain toDomain() {
return new Domain(domainId, rootDomain);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.seong.shoutlink.domain.domain.repository;

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

public interface DomainJpaRepository extends JpaRepository<DomainEntity, Long> {

Optional<DomainEntity> findByRootDomain(String rootDomain);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.seong.shoutlink.domain.domain.repository;

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.domain.service.DomainRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class DomainRepositoryImpl implements DomainRepository {

private final DomainJpaRepository domainJpaRepository;

@Override
public Optional<Domain> findByRootDomain(String rootDomain) {
return domainJpaRepository.findByRootDomain(rootDomain)
.map(DomainEntity::toDomain);
}

@Override
public Domain save(Domain domain) {
return domainJpaRepository.save(DomainEntity.create(domain))
.toDomain();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.seong.shoutlink.domain.domain.service;

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

public interface DomainRepository {

Optional<Domain> findByRootDomain(String rootDomain);

Domain save(Domain domain);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.seong.shoutlink.domain.domain.service;

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.domain.service.request.UpdateDomainCommand;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class DomainService {

private final DomainRepository domainRepository;
private final LinkRepository linkRepository;

@Transactional
public UpdateDomainResponse updateDomain(UpdateDomainCommand command) {
String rootDomain = DomainExtractor.extractRootDomain(command.url());
Domain domain = domainRepository.findByRootDomain(rootDomain)
.orElseGet(() -> {
Domain newDomain = new Domain(rootDomain);
return domainRepository.save(newDomain);
});
Link link = linkRepository.findById(command.linkId())
.orElseThrow(() -> new ShoutLinkException("존재하지 않는 링크입니다.", ErrorCode.NOT_FOUND));
linkRepository.updateLinkDomain(link, domain);
return new UpdateDomainResponse(domain.getDomainId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.seong.shoutlink.domain.domain.service.request;

public record UpdateDomainCommand(Long linkId, String url) {

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

public record UpdateDomainResponse(Long domainId) {

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

import com.seong.shoutlink.domain.exception.ErrorCode;
import com.seong.shoutlink.domain.exception.ShoutLinkException;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class DomainExtractor {

private static final String HTTP = "http";
private static final Pattern URL_PATTERN = Pattern.compile(
"[-a-zA-Z0-9@:%_\\+.~#?&//=]{2,256}\\.[a-z]{2,4}\\b(\\/[-a-zA-Z0-9가-힣@:%_\\+.~#?&//=]*)?");
private static final Set<String> SECOND_DOMAIN = new HashSet<>();

static {
SECOND_DOMAIN.addAll(
List.of("co", "ne", "or", "re", "pe", "go", "mil", "ac", "hs", "ms", "es", "sc", "kg"));
}

public static String extractRootDomain(String url) {
checkIsUrlPattern(url);
String fullDomain = removePath(url);
return removeSubDomain(fullDomain);
}

private static void checkIsUrlPattern(String url) {
if(URL_PATTERN.matcher(url).matches()) {
return;
}
throw new ShoutLinkException("입력값이 URL 형식이 아닙니다.", ErrorCode.ILLEGAL_ARGUMENT);
}

private static String removePath(String url) {
if(url.startsWith(HTTP)) {
URI uri = URI.create(url);
return uri.getHost();
}
int firstSlashIndex = url.indexOf("/");
if(firstSlashIndex == -1) {
return url;
}
return url.substring(0, firstSlashIndex);
}

private static String removeSubDomain(String fullDomain) {
String[] splitDomain = fullDomain.split("\\.");
if(hasSecondDomain(splitDomain)) {
return makeRootDomain(3, splitDomain);
} else {
return makeRootDomain(2, splitDomain);
}
}

private static boolean hasSecondDomain(String[] splitDomain) {
int totalDepth = splitDomain.length;
if(totalDepth > 2) {
String secondDomain = splitDomain[totalDepth - 2];
return SECOND_DOMAIN.stream()
.anyMatch(secondDomain::equals);
}
return false;
}

private static String makeRootDomain(int rootDomainDepth, String[] splitDomain) {
int totalDepth = splitDomain.length;
StringBuilder domainBuilder = new StringBuilder();
for (int depth = rootDomainDepth; depth > 0; depth--) {
domainBuilder.append(splitDomain[totalDepth - depth]);
if(depth > 1) {
domainBuilder.append(".");
}
}
return domainBuilder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.seong.shoutlink.domain.link.repository;

import com.seong.shoutlink.domain.common.BaseEntity;
import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.link.Link;
import com.seong.shoutlink.domain.link.LinkWithLinkBundle;
import com.seong.shoutlink.domain.linkbundle.LinkBundle;
Expand All @@ -26,6 +27,7 @@ public class LinkEntity extends BaseEntity {
private String url;
private String description;
private Long linkBundleId;
private Long domainId;

private LinkEntity(String url, String description, Long linkBundleId) {
this.url = url;
Expand All @@ -45,4 +47,8 @@ public static LinkEntity create(LinkWithLinkBundle linkWithLinkBundle) {
public Link toDomain() {
return new Link(linkId, url, description);
}

public void updateDomainId(Domain domain) {
domainId = domain.getDomainId();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.seong.shoutlink.domain.link.repository;

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.link.Link;
import com.seong.shoutlink.domain.link.LinkWithLinkBundle;
import com.seong.shoutlink.domain.link.service.LinkRepository;
import com.seong.shoutlink.domain.link.service.result.LinkPaginationResult;
import com.seong.shoutlink.domain.linkbundle.LinkBundle;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand Down Expand Up @@ -38,4 +40,16 @@ public LinkPaginationResult findLinks(
linkEntityPage.getTotalElements(),
linkEntityPage.hasNext());
}

@Override
public void updateLinkDomain(Link link, Domain domain) {
linkJpaRepository.findById(link.getLinkId())
.ifPresent(linkEntity -> linkEntity.updateDomainId(domain));
}

@Override
public Optional<Link> findById(Long linkId) {
return linkJpaRepository.findById(linkId)
.map(LinkEntity::toDomain);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.seong.shoutlink.domain.link.service;

import com.seong.shoutlink.domain.domain.Domain;
import com.seong.shoutlink.domain.link.Link;
import com.seong.shoutlink.domain.link.LinkWithLinkBundle;
import com.seong.shoutlink.domain.link.service.result.LinkPaginationResult;
import com.seong.shoutlink.domain.linkbundle.LinkBundle;
import java.util.Optional;

public interface LinkRepository {

Long save(LinkWithLinkBundle linkWithLinkBundle);

LinkPaginationResult findLinks(LinkBundle linkBundle, int page, int size);

void updateLinkDomain(Link link, Domain domain);

Optional<Link> findById(Long linkId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public CreateLinkResponse createLink(CreateLinkCommand command) {
Link link = new Link(command.url(), command.description());
LinkWithLinkBundle linkWithLinkBundle = new LinkWithLinkBundle(link, linkBundle);
Long linkId = linkRepository.save(linkWithLinkBundle);
eventPublisher.publishEvent(new CreateLinkEvent(command.url()));
eventPublisher.publishEvent(new CreateLinkEvent(linkId, command.url()));
return new CreateLinkResponse(linkId);
}

Expand Down Expand Up @@ -78,7 +78,7 @@ public CreateHubLinkResponse createHubLink(CreateHubLinkCommand command) {
LinkBundle hubLinkBundle = getHubLinkBundle(command.linkBundleId(), hub);
Link link = new Link(command.url(), command.description());
Long linkId = linkRepository.save(new LinkWithLinkBundle(link, hubLinkBundle));
eventPublisher.publishEvent(new CreateLinkEvent(command.url()));
eventPublisher.publishEvent(new CreateLinkEvent(linkId, command.url()));
return new CreateHubLinkResponse(linkId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

import com.seong.shoutlink.domain.common.Event;

public record CreateLinkEvent(String url) implements Event {
public record CreateLinkEvent(Long linkId, String url) implements Event {

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.seong.shoutlink.global.config;

import com.seong.shoutlink.domain.common.EventPublisher;
import com.seong.shoutlink.domain.domain.service.DomainService;
import com.seong.shoutlink.domain.linkbundle.service.LinkBundleService;
import com.seong.shoutlink.global.event.DomainEventListener;
import com.seong.shoutlink.global.event.LinkBundleEventListener;
import com.seong.shoutlink.global.event.SpringEventPublisher;
import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -20,4 +22,9 @@ public EventPublisher eventPublisher(ApplicationEventPublisher applicationEventP
public LinkBundleEventListener springEventListener(LinkBundleService linkBundleService) {
return new LinkBundleEventListener(linkBundleService);
}

@Bean
public DomainEventListener domainEventListener(DomainService domainService) {
return new DomainEventListener(domainService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.seong.shoutlink.global.event;

import com.seong.shoutlink.domain.domain.service.DomainService;
import com.seong.shoutlink.domain.domain.service.request.UpdateDomainCommand;
import com.seong.shoutlink.domain.link.service.event.CreateLinkEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

@RequiredArgsConstructor
public class DomainEventListener {

private final DomainService domainService;

@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateDomainInfo(CreateLinkEvent event) {
domainService.updateDomain(new UpdateDomainCommand(event.linkId(), event.url()));
}
}
Loading

0 comments on commit 2ac0c07

Please sign in to comment.