diff --git a/linkmind/src/main/java/com/app/toaster/parse/service/ParsingService.java b/linkmind/src/main/java/com/app/toaster/parse/service/ParsingService.java index 758674ff..f7f63abf 100644 --- a/linkmind/src/main/java/com/app/toaster/parse/service/ParsingService.java +++ b/linkmind/src/main/java/com/app/toaster/parse/service/ParsingService.java @@ -31,45 +31,57 @@ public ParsingService(@Value("${static-image.url}") final String basicThumbnail) this.BASIC_THUMBNAIL = basicThumbnail; } - public OgResponse getOg(String linkUrl) throws IOException { - try { - String title = getTitle(linkUrl); - log.info(title); - String image = getImage(linkUrl); - log.info(image); - return OgResponse.of( - title == null || title.isBlank() ? "기본 토스트 제목" : title, - image == null || image.isBlank() ? BASIC_THUMBNAIL : image - ); - }catch (HttpStatusException | SSLHandshakeException e){ - return OgResponse.of("15자 내로 제목을 지어주세요.", BASIC_THUMBNAIL); - }catch (ConnectException e){ - throw new BadRequestException(Error.BAD_REQUEST_URL, Error.BAD_REQUEST_URL.getMessage()); - } + public OgResponse getOg(String linkUrl) { + String title = getTitle(linkUrl); + log.info(title); + String image = getImage(linkUrl); + log.info(image); + return OgResponse.of( + title == null || title.isBlank() ? "기본 토스트 제목" : title, + image == null || image.isBlank() ? BASIC_THUMBNAIL : image + ); } // public String getOg(String linkUrl) throws IOException { // String image = getImage(linkUrl); // return image == null || image.isBlank() ? BASIC_THUMBNAIL : image; // } - private String getTitle(String linkUrl) throws IOException { + private String getTitle(String linkUrl) { try { - Document doc = Jsoup.connect(linkUrl).get(); + Document doc = Jsoup.connect(linkUrl) + .followRedirects(true) // 리다이렉션 자동 따라가기 + .maxBodySize(1024*1024) // 페이지 크기 제한 없음 + .timeout(10000) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .get(); Elements ogTitleElements = doc.select("meta[property=og:title]"); Elements titleElements = doc.select("head").select("title"); if (ogTitleElements.isEmpty() && titleElements.isEmpty()) { - return null; + log.info("[NOT FOUND] og 데이터, html header 뜯었는데 결과 없음."); + return "15자 내로 제목을 지어주세요."; } return ogTitleElements.isEmpty()?titleElements.get(0).text(): ogTitleElements.get(0).attr("content"); }catch (org.jsoup.HttpStatusException e){ - return null; + log.info("[ERROR] title 파싱 중 http status 에러 발생"); + return "15자 내로 제목을 지어주세요."; + } catch (SSLHandshakeException e){ + log.info("[ERROR] 너무 오래된 사이트라 handshake 규칙이 맞지 않습니다."); + return "15자 내로 제목을 지어주세요."; + } catch (IOException e){ + log.info("[ERROR] title 파싱 중 에러 발생"); + return "15자 내로 제목을 지어주세요."; } - } private String getImage(String linkUrl){ try { - Document doc = Jsoup.connect(linkUrl).get(); + Document doc = Jsoup.connect(linkUrl) + .followRedirects(true) // 리다이렉션 자동 따라가기 + .maxBodySize(1024*1024) // 페이지 크기 제한 없음 + .timeout(10000) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .get(); + Elements iframes = doc.select("iframe"); Elements ogBlogImage = new Elements(); if (!iframes.isEmpty()){ @@ -82,7 +94,10 @@ private String getImage(String linkUrl){ return findImageAnywhere(ogImageElements, ogImage, ogBlogImage); }catch (MalformedURLException e){ throw new CustomException(Error.MALFORMED_URL_EXEPTION,Error.MALFORMED_URL_EXEPTION.getMessage()); - }catch (org.jsoup.HttpStatusException e){ + }catch (org.jsoup.HttpStatusException e) { + return null; + }catch (SSLHandshakeException e){ + log.info("[ERROR] 너무 오래된 사이트라 handshake 규칙이 맞지 않습니다."); return null; }catch (IOException e){ throw new CustomException(Error.NOT_FOUND_IMAGE_EXCEPTION, Error.NOT_FOUND_IMAGE_EXCEPTION.getMessage()); diff --git a/linkmind/src/main/java/com/app/toaster/toast/controller/ToastController.java b/linkmind/src/main/java/com/app/toaster/toast/controller/ToastController.java index 096b9265..6b029a34 100644 --- a/linkmind/src/main/java/com/app/toaster/toast/controller/ToastController.java +++ b/linkmind/src/main/java/com/app/toaster/toast/controller/ToastController.java @@ -42,7 +42,7 @@ public class ToastController { @Deprecated public ApiResponse getOgAdvanced( @RequestBody OgRequestDto ogRequestDto - ) throws IOException { + ) { return ApiResponse.success(Success.PARSING_OG_SUCCESS, parsingService.getOg(ogRequestDto.linkUrl())); } @@ -72,7 +72,7 @@ public ApiResponse updateIsRead( public ApiResponse deleteToast( //나중에 softDelete로 변경 @UserId Long userId, @RequestParam Long toastId - ) throws IOException { + ) { toastService.deleteToast(userId, toastId); return ApiResponse.success(Success.DELETE_TOAST_SUCCESS); } diff --git a/linkmind/src/main/java/com/app/toaster/toast/service/ToastService.java b/linkmind/src/main/java/com/app/toaster/toast/service/ToastService.java index 1b401f4f..3091da1f 100644 --- a/linkmind/src/main/java/com/app/toaster/toast/service/ToastService.java +++ b/linkmind/src/main/java/com/app/toaster/toast/service/ToastService.java @@ -55,30 +55,21 @@ public void createToast(Long userId, SaveToastDto saveToastDto){ throw new CustomException(Error.BAD_REQUEST_EMPTY_URL, Error.BAD_REQUEST_EMPTY_URL.getMessage()); } //토스트 생성 - try { - OgResponse res = parsingService.getOg(saveToastDto.linkUrl()); - //byte 배열로 읽어들임. - log.info(res.titleAdvanced()); - log.info(res.imageAdvanced()); - String imageString = checkIsBasicImage(res.imageAdvanced()); - // // ImagePresignedUrlResponse realRes = getUploadPreSignedUrl(res.imageAdvanced()); - // log.info(realRes.fileName()); - // log.info(realRes.preSignedUrl()); - - - //presigned url - Toast toast = Toast.builder() - .user(presentUser) - .linkUrl(saveToastDto.linkUrl()) - .title(res.titleAdvanced()) - .thumbnailUrl(imageString) - .build(); - // 만약 유저에게 만들어져있는 카테고리가 없는지 확인하고 - checkCategoryIsEmpty(toast, saveToastDto.categoryId()); - toastRepository.save(toast); - } catch (IOException e ) { //여기서 에러 발생 시 외부 s3 문제일 수 도 있으므로 500으로 에러 예상 범위 알림. - throw new CustomException(Error.CREATE_TOAST_PROCCESS_EXCEPTION, Error.CREATE_TOAST_PROCCESS_EXCEPTION.getMessage()); - } + OgResponse res = parsingService.getOg(saveToastDto.linkUrl()); + //byte 배열로 읽어들임. + log.info(res.titleAdvanced()); + log.info(res.imageAdvanced()); + String imageString = checkIsBasicImage(res.imageAdvanced()); + + Toast toast = Toast.builder() + .user(presentUser) + .linkUrl(saveToastDto.linkUrl()) + .title(res.titleAdvanced()) + .thumbnailUrl(imageString) + .build(); + // 만약 유저에게 만들어져있는 카테고리가 없는지 확인하고 + checkCategoryIsEmpty(toast, saveToastDto.categoryId()); + toastRepository.save(toast); } @Transactional diff --git a/linkmind/src/test/java/com/app/toaster/parse/service/ParsingServiceTest.java b/linkmind/src/test/java/com/app/toaster/parse/service/ParsingServiceTest.java new file mode 100644 index 00000000..cd71a3f1 --- /dev/null +++ b/linkmind/src/test/java/com/app/toaster/parse/service/ParsingServiceTest.java @@ -0,0 +1,62 @@ +package com.app.toaster.parse.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.app.toaster.parse.controller.response.OgResponse; +import com.app.toaster.toast.controller.request.SaveToastDto; +import java.io.IOException; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ParsingServiceTest { + + @InjectMocks + private ParsingService parsingService; + + @Test + @DisplayName("리다이렉션 Url에 대해서도 open graph 파싱이 잘된다.") + void getOgWhenRedirect302Url() throws IOException { + // given + String redirectUrl = createRedirect302CaseFixture().linkUrl(); + + // when - 직접 Jsoup으로 테스트 + Document doc = Jsoup.connect(redirectUrl) + .followRedirects(true) + .maxBodySize(1024 * 1024) + .timeout(10000) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .get(); + + Elements ogTitleElements = doc.select("meta[property=og:title]"); + Elements ogImageElements = doc.select("meta[property=og:image]"); + Elements titleElements = doc.select("title"); + + // then - Jsoup 레벨에서 먼저 검증 + System.out.println("OG Title: " + (ogTitleElements.isEmpty() ? "없음" : ogTitleElements.attr("content"))); + System.out.println("OG Image: " + (ogImageElements.isEmpty() ? "없음" : ogImageElements.attr("content"))); + System.out.println("Title: " + (titleElements.isEmpty() ? "없음" : titleElements.text())); + + // when - 서비스 레벨 테스트 + OgResponse result = parsingService.getOg(redirectUrl); + + // then + assertThat(result).isNotNull(); + assertThat(result.titleAdvanced()).isNotBlank(); + assertThat(result.imageAdvanced()).isNotBlank(); + + // 추가 검증 + assertThat(result.titleAdvanced()).isNotEqualTo("기본 토스트 제목"); + assertThat(result.imageAdvanced()).isNotEqualTo("BASIC_THUMBNAIL_URL"); + } + + private SaveToastDto createRedirect302CaseFixture(){ + return new SaveToastDto("https://digital.mk.co.kr/news_link.php?year=2025&no=469576", 1L); + } +} \ No newline at end of file diff --git a/linkmind/src/test/java/com/app/toaster/toast/service/ToastServiceTest.java b/linkmind/src/test/java/com/app/toaster/toast/service/ToastServiceTest.java index 18e4938d..bae388e5 100644 --- a/linkmind/src/test/java/com/app/toaster/toast/service/ToastServiceTest.java +++ b/linkmind/src/test/java/com/app/toaster/toast/service/ToastServiceTest.java @@ -71,7 +71,7 @@ void setUp() { @Test @DisplayName("Toast 생성 성공 테스트") - void createToast_Success() throws IOException { + void createToast_Success() { // given Long userId = 1L;