Skip to content

Commit

Permalink
[REFACTOR] Pick 존재 여부 확인 API + OpenGraph 획득 방식 리팩토링 (#699)
Browse files Browse the repository at this point in the history
* ✨ feat: OpenGraph 획득 클래스 분리

* chore: 매직넘버는 var 말고 타입 명시

* refactor: openGraph 클래스로 리팩토링

* fix: 프론트엔드 요청 반영 (url 픽 존재 여부 확인 API)

* chore: 미사용 메서드 제거

* chore: 미사용 import 제거

* refactor: openGraph 반복 코드 리팩토링

* chore: 주석 추가
  • Loading branch information
kimminkyeu authored Dec 9, 2024
1 parent 78a431f commit 9a2bb2e
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public ResponseEntity<List<PickApiResponse.FolderPickList>> getFolderChildPickLi

return ResponseEntity.ok(
folderPickList.stream()
.map(pickApiMapper::toApiFolderPickList)
.toList());
.map(pickApiMapper::toApiFolderPickList)
.toList());
}

@GetMapping("/search")
Expand All @@ -71,11 +71,15 @@ public ResponseEntity<List<PickApiResponse.FolderPickList>> getFolderChildPickLi
})
public ResponseEntity<PickSliceResponse<PickApiResponse.Pick>> searchPickPagination(
@LoginUserId Long userId,
@Parameter(description = "조회할 폴더 ID 목록", example = "1, 2, 3") @RequestParam(required = false, defaultValue = "") List<Long> folderIdList,
@Parameter(description = "검색 토큰 목록", example = "리액트, 쿼리, 서버") @RequestParam(required = false, defaultValue = "") List<String> searchTokenList,
@Parameter(description = "검색 태그 ID 목록", example = "1, 2, 3") @RequestParam(required = false, defaultValue = "") List<Long> tagIdList,
@Parameter(description = "조회할 폴더 ID 목록", example = "1, 2, 3") @RequestParam(required = false, defaultValue =
"") List<Long> folderIdList,
@Parameter(description = "검색 토큰 목록", example = "리액트, 쿼리, 서버") @RequestParam(required = false, defaultValue =
"") List<String> searchTokenList,
@Parameter(description = "검색 태그 ID 목록", example = "1, 2, 3") @RequestParam(required = false,
defaultValue = "") List<Long> tagIdList,
@Parameter(description = "픽 시작 id 조회", example = "0") @RequestParam(required = false, defaultValue = "0") Long cursor,
@Parameter(description = "한 페이지에 가져올 픽 개수", example = "20") @RequestParam(required = false, defaultValue = "20") int size
@Parameter(description = "한 페이지에 가져올 픽 개수", example = "20") @RequestParam(required = false, defaultValue = "20"
) int size
) {
Slice<PickResult.Pick> pickResultList = pickSearchService.searchPickPagination(
pickApiMapper.toSearchPaginationCommand(userId, folderIdList, searchTokenList, tagIdList, cursor, size));
Expand All @@ -96,13 +100,24 @@ public ResponseEntity<List<PickApiResponse.Pick>> searchPick(
pickApiMapper.toSearchCommand(userId, request));

List<PickApiResponse.Pick> pickResponseList = pickList.stream()
.map(pickApiMapper::toApiResponse)
.toList();
.map(pickApiMapper::toApiResponse)
.toList();
return ResponseEntity.ok(pickResponseList);
}

/**
* @deprecated
* 현재 익스텐션에서만 사용되며, 추후 없어질 예정입니다.
*/
@Deprecated
@GetMapping("/link")
@Operation(summary = "링크 픽 여부 조회", description = "해당 링크를 픽한 적이 있는지 확인합니다.")
@Operation(
summary = "[Deprecated] 링크 픽 여부 조회",
description = """
해당 링크를 픽한 적이 있는지 확인합니다.
픽이 존재하지 않음을 4XX로 판단하는 것이 프론트엔드에서 처리하기 까다로워
Deprecated 처리하였습니다.
""")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "픽 여부 조회 성공"),
@ApiResponse(responseCode = "404", description = "해당 링크에 대해 픽이 되어 있지 않습니다.")
Expand All @@ -112,6 +127,27 @@ public ResponseEntity<PickApiResponse.Pick> getPickUrl(@LoginUserId Long userId,
return ResponseEntity.ok(pickApiMapper.toApiResponse(pickService.getPickUrl(userId, link)));
}

/**
* @author minkyeu kim
* 프론트엔드에서 4XX를 픽 없음으로 판단하는 로직이 불편하기 때문에
* Boolean으로 픽이 있다 없다를 판단하도록 변경합니다.
* 익스텐션은 구글 심사 때문에 기존 getLinkDataXXX를 이용하고, 추후 getLinkDataV2 로 교체할 예정
*/
@GetMapping("/link-v2")
@Operation(summary = "링크 픽 여부 조회", description = "해당 링크를 픽한 적이 있는지 확인합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "픽 여부 조회 성공"),
})
public ResponseEntity<PickApiResponse.PickExists> doesUserHasPickWithGivenUrl(
@LoginUserId Long userId, @RequestParam String link
) {
var response = pickService.findPickUrl(userId, link)
.map(pickApiMapper::toApiResponse)
.map(pick -> new PickApiResponse.PickExists(true, pick))
.orElseGet(() -> new PickApiResponse.PickExists(false, null));
return ResponseEntity.ok(response);
}

@GetMapping("/{id}")
@Operation(summary = "픽 상세 조회", description = "픽을 상세 조회합니다.")
@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public record FolderPickList(
) {
}

public record PickExists(
Boolean exist,
PickApiResponse.Pick pick
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class RankingApiController {
})
public ResponseEntity<RankingByViewCount> getSuggestionByViewCount(
) {
var LIMIT = 10;
Integer LIMIT = 10;
var currentDay = LocalDate.now();
var before1Day = currentDay.minusDays(1);
var before7Days = currentDay.minusDays(7);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package techpick.api.domain.link.service;

import java.io.IOException;

import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -15,93 +9,49 @@
import techpick.api.domain.link.dto.LinkResult;
import techpick.api.domain.link.exception.ApiLinkException;
import techpick.api.infrastructure.link.LinkDataHandler;
import techpick.api.lib.opengraph.Metadata;
import techpick.api.lib.opengraph.OpenGraph;
import techpick.api.lib.opengraph.OpenGraphException;
import techpick.core.model.link.Link;

@Slf4j
@Service
@RequiredArgsConstructor
@Slf4j
public class LinkService {

private final LinkDataHandler linkDataHandler;
private final LinkMapper linkMapper;

@Transactional(readOnly = true)
public LinkResult getLinkInfo(String url) {
Link link = linkDataHandler.getLink(url);
return linkMapper.toLinkResult(link);
}

@Transactional
public void updateOgTag(String url) {
Link link = linkDataHandler.getOptionalLink(url).orElseGet(() -> Link.createLinkByUrl(url));
try {
String html = getJsoupResponse(url).body();

Document document = Jsoup.parse(html);

String title = getTitle(document);
String description = getMetaContent(document, "og:description");
String imageUrl = correctImageUrl(url, getMetaContent(document, "og:image"));

link.updateMetadata(title, description, imageUrl);
linkDataHandler.saveLink(link);
var updatedLink = updateOpengraph(url, link);
linkDataHandler.saveLink(updatedLink);
} catch (Exception e) {
log.info("url : {} 의 og tag 추출에 실패했습니다.", url);
log.info("url : {} 의 og tag 추출에 실패했습니다.", url, e);
}
}

@Transactional
public LinkResult getUpdateOgTag(String url) {
Link link = linkDataHandler.getOptionalLink(url).orElseGet(() -> Link.createLinkByUrl(url));
try {
String html = getJsoupResponse(url).body();

Document document = Jsoup.parse(html);

String title = getTitle(document);
String description = getMetaContent(document, "og:description");
String imageUrl = correctImageUrl(url, getMetaContent(document, "og:image"));

link.updateMetadata(title, description, imageUrl);
return linkMapper.toLinkResult(linkDataHandler.saveLink(link));
var updatedLink = updateOpengraph(url, link);
return linkMapper.toLinkResult(linkDataHandler.saveLink(updatedLink));
} catch (Exception e) {
throw ApiLinkException.LINK_OG_TAG_UPDATE_FAILURE();
}
}

private Connection.Response getJsoupResponse(String url) throws IOException {
return Jsoup.connect(url)
.userAgent(
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0"
+ ".1667.0 Safari/537.36")
.header("scheme", "https")
.header("accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
.header("accept-language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6")
.header("cache-control", "no-cache")
.header("pragma", "no-cache")
.header("upgrade-insecure-requests", "1")
.execute();
}

/**
* meta tag 에서 정보 가져옴. 존재하지 않으면 빈 스트링("") 반환
* */
private String getMetaContent(Document document, String propertyName) {
Element metaTag = document.selectFirst("meta[property=" + propertyName + "]");
return metaTag != null ? metaTag.attr("content") : "";
}

/**
* og:title 이 존재하지 않으면 title 태그에서 가져옴
* title 태그 또한 존재하지 않으면 빈스트링("") 반환
* */
private String getTitle(Document document) {
String title = getMetaContent(document, "og:title");
if (title.isEmpty()) {
title = document.title();
}
return title;
private Link updateOpengraph(String url, Link link) throws OpenGraphException {
var openGraph = new OpenGraph(url);
link.updateMetadata(
openGraph.getTag(Metadata.TITLE).orElse(""),
openGraph.getTag(Metadata.DESCRIPTION).orElse(""),
correctImageUrl(url, openGraph.getTag(Metadata.IMAGE).orElse(""))
);
return link;
}

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

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

import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -48,6 +49,11 @@ public PickResult.Pick getPickUrl(Long userId, String url) {
return pickMapper.toPickResult(pick);
}

@Transactional(readOnly = true)
public Optional<PickResult.Pick> findPickUrl(Long userId, String url) {
return pickDataHandler.findPickUrl(userId, url).map(pickMapper::toPickResult);
}

// 폴더 내에 있는 픽 리스트 조회
// 구현은 해두었지만, 추후 사용되지 않을 때 삭제 예정
@Transactional(readOnly = true)
Expand All @@ -57,17 +63,17 @@ public List<PickResult.Pick> getFolderChildPickList(Long userId, Long folderId)
List<Pick> pickList = pickDataHandler.getPickListPreservingOrder(folder.getChildPickIdOrderedList());

return pickList.stream()
.map(pickMapper::toPickResult)
.toList();
.map(pickMapper::toPickResult)
.toList();
}

// 폴더 리스트가 넘어오면, 각 폴더 내부에 있는 픽 리스트 조회
@Transactional(readOnly = true)
public List<PickResult.FolderPickList> getFolderListChildPickList(PickCommand.ReadList command) {
return command.folderIdList().stream()
.peek(folderId -> validateFolderAccess(command.userId(), folderId)) // 폴더 접근 유효성 검사
.map(this::getFolderChildPickResultList)
.toList();
.peek(folderId -> validateFolderAccess(command.userId(), folderId)) // 폴더 접근 유효성 검사
.map(this::getFolderChildPickResultList)
.toList();
}

@LoginUserIdDistributedLock
Expand Down Expand Up @@ -126,8 +132,8 @@ private PickResult.FolderPickList getFolderChildPickResultList(Long folderId) {
Folder folder = folderDataHandler.getFolder(folderId);
List<Pick> pickList = pickDataHandler.getPickListPreservingOrder(folder.getChildPickIdOrderedList());
List<PickResult.Pick> pickResultList = pickList.stream()
.map(pickMapper::toPickResult)
.toList();
.map(pickMapper::toPickResult)
.toList();
return pickMapper.toPickResultList(folderId, pickResultList);
}

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

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

import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -56,6 +54,11 @@ public Pick getPickUrl(Long userId, String url) {
.orElseThrow(ApiPickException::PICK_NOT_FOUND);
}

@Transactional(readOnly = true)
public Optional<Pick> findPickUrl(Long userId, String url) {
return pickRepository.findByUserIdAndLinkUrl(userId, url);
}

@Transactional(readOnly = true)
public List<Pick> getPickList(List<Long> pickIdList) {
List<Pick> pickList = pickRepository.findAllById_JoinLink(pickIdList);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package techpick.api.lib.opengraph;

/**
* @author minkyeu kim
* OpenGraph 표준에 따른 데이터 형식
* 참고 - https://ogp.me/
*/
public class Metadata {

/***************************************
* Basic Metadata (required)
***************************************/

// The title of your object as it should appear within the graph, e.g., "The Rock".
public static final MetadataTag TITLE = MetadataTag.of("og:title");

// The type of your object, e.g., "video.movie".
// Depending on the type you specify, other properties may also be required.
public static final MetadataTag TYPE = MetadataTag.of("og:type");

// An image URL which should represent your object within the graph.
public static final MetadataTag IMAGE = MetadataTag.of("og:image");

// The canonical URL of your object that will be used as its permanent ID in the graph, e.g., "https://www.imdb.com/title/tt0117500/".
public static final MetadataTag URL = MetadataTag.of("og:url");

/***************************************
* Optional Metadata
***************************************/

// A URL to an audio file to accompany this object.
public static final MetadataTag AUDIO = MetadataTag.of("og:audio");

// A one to two sentence description of your object.
public static final MetadataTag DESCRIPTION = MetadataTag.of("og:description");

// The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is
// chosen, the consumer of your data should chose between "a" or "an". Default is "" (blank).
public static final MetadataTag DETERMINER = MetadataTag.of("og:determiner");

// The locale these tags are marked up in. Of the format language_TERRITORY. Default is en_US.
public static final MetadataTag LOCALE = MetadataTag.of("og:locale");

// An array of other locales this page is available in.
public static final MetadataTag LOCALE_ALTERNATE = MetadataTag.of("og:locale:alternate");

// If your object is part of a larger web site, the name which should be
// displayed for the overall site. e.g.,"IMDb".
public static final MetadataTag SITE_NAME = MetadataTag.of("og:site_name");

// A URL to a video file that complements this object.
public static final MetadataTag VIDEO = MetadataTag.of("og:video");

/**
* VO for metadata key
*/
public record MetadataTag(String key) {
public static MetadataTag of(String key) {
return new MetadataTag(key);
}
}
}
Loading

0 comments on commit 9a2bb2e

Please sign in to comment.