diff --git a/src/main/java/dgu/newsee/domain/crawlednews/service/NewsService.java b/src/main/java/dgu/newsee/domain/crawlednews/service/NewsService.java index eb7649f..85f238a 100644 --- a/src/main/java/dgu/newsee/domain/crawlednews/service/NewsService.java +++ b/src/main/java/dgu/newsee/domain/crawlednews/service/NewsService.java @@ -9,6 +9,9 @@ import dgu.newsee.domain.transformednews.service.TransformedNewsService; import dgu.newsee.domain.user.entity.User; import dgu.newsee.domain.user.repository.UserRepository; +import dgu.newsee.global.exception.NewsException; +import dgu.newsee.global.exception.UserException; +import dgu.newsee.global.payload.ResponseCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,7 +33,7 @@ public NewsOrigin crawlAndSave(NewsCrawlRequestDTO request, Long userId) { // 사용자 조회 User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + .orElseThrow(() -> new UserException(ResponseCode.USER_UNAUTHORIZED)); // 1. 이미 저장된 뉴스면 바로 반환 @@ -63,7 +66,7 @@ public NewsOrigin crawlAndSave(NewsCrawlRequestDTO request, Long userId) { return newsOrigin; } catch (Exception e) { - throw new RuntimeException("크롤링 실패: " + e.getMessage()); + throw new NewsException(ResponseCode.NEWS_CRAWL_FAIL); } } } diff --git a/src/main/java/dgu/newsee/domain/crawlednews/util/NewsParserUtil.java b/src/main/java/dgu/newsee/domain/crawlednews/util/NewsParserUtil.java index 7e3b410..7b76e34 100644 --- a/src/main/java/dgu/newsee/domain/crawlednews/util/NewsParserUtil.java +++ b/src/main/java/dgu/newsee/domain/crawlednews/util/NewsParserUtil.java @@ -16,35 +16,41 @@ public static ParsedNews parse(Document doc, String categoryFromCaller, String u String title = doc.select("meta[property=og:title]").attr("content"); // 본문 - // 본문 파싱 (p 태그 우선, 없으면 br 기준으로 직접 파싱) String content = ""; - Elements paragraphs = doc.select("#dic_area > p"); - if (!paragraphs.isEmpty()) { - List lines = new ArrayList<>(); - for (Element p : paragraphs) { - String text = p.text().trim(); - if (!text.isEmpty()) lines.add(text); - } - content = String.join("\n", lines); - } else { - // fallback: br 태그 기준으로 수동 파싱 - Element dicArea = doc.selectFirst("#dic_area"); - if (dicArea != null) { - StringBuilder builder = new StringBuilder(); - for (var node : dicArea.childNodes()) { - if (node.nodeName().equals("br")) { - builder.append("\n"); - } else { - builder.append(node.toString().replaceAll("<.*?>", "").trim()); + Element dicArea = doc.selectFirst("#dic_area"); + if (dicArea != null) { + // HTML 전체를 가져와서
두 개 이상을 기준으로 문단 나누기 + String rawHtml = dicArea.html(); + + //
태그를 통일된 형태로 바꿔 처리하기 쉽게 함 + rawHtml = rawHtml.replaceAll("(?i)]*>", "
"); + + // 연속된

을 기준으로 문단 나누기 + String[] paragraphsRaw = rawHtml.split("(
\\s*){2,}"); + + StringBuilder contentBuilder = new StringBuilder(); + for (String paragraphHtml : paragraphsRaw) { + //
단일은 줄바꿈, 나머지는 태그 제거 + String paragraphText = paragraphHtml + .replaceAll("(
\\s*)+", "\n") // 단일
은 줄바꿈 + .replaceAll("<[^>]+>", "") // 나머지 HTML 태그 제거 + .trim(); + + if (!paragraphText.isEmpty()) { + if (contentBuilder.length() > 0) { + contentBuilder.append("\n\n"); // 단락 구분 } + contentBuilder.append(paragraphText); } - content = builder.toString().replaceAll("\n{2,}", "\n"); // 줄바꿈 2번 이상은 하나로 줄이기 } + + content = contentBuilder.toString(); } + // 출처 String source = doc.select("meta[property=og:article:author]").attr("content"); if (source.isBlank()) { diff --git a/src/main/java/dgu/newsee/global/payload/ResponseCode.java b/src/main/java/dgu/newsee/global/payload/ResponseCode.java index c7e299c..916d172 100644 --- a/src/main/java/dgu/newsee/global/payload/ResponseCode.java +++ b/src/main/java/dgu/newsee/global/payload/ResponseCode.java @@ -27,6 +27,7 @@ public enum ResponseCode implements BaseErrorCode { NEWS_SAVE_SUCCESS(ResponseCodeType.SUCCESS, "NEWS201", "뉴스 저장에 성공했습니다.", HttpStatus.OK), NEWS_DELETE_SUCCESS(ResponseCodeType.SUCCESS, "NEWS202", "뉴스 삭제에 성공했습니다.", HttpStatus.OK), NEWS_SEARCH_SUCCESS(ResponseCodeType.SUCCESS, "NEWS203", "뉴스 검색 성공", HttpStatus.OK), + NEWS_CRAWL_FAIL(ResponseCodeType.ERROR, "NEWS500", "뉴스 크롤링에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), // ✅ 단어장 관련 WORD_SAVE_SUCCESS(ResponseCodeType.SUCCESS, "WORD201", "단어 저장에 성공했습니다.", HttpStatus.OK), @@ -46,8 +47,13 @@ public enum ResponseCode implements BaseErrorCode { INVALID_REQUEST(ResponseCodeType.ERROR, "REQ400", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST), MISSING_PARAMETER(ResponseCodeType.ERROR, "REQ401", "필수 파라미터가 누락되었습니다.", HttpStatus.BAD_REQUEST), PARSE_ERROR(ResponseCodeType.ERROR, "REQ402", "데이터 파싱 오류입니다.", HttpStatus.BAD_REQUEST), + + // ✅ AI관련 AI_SERVER_DOWN(ResponseCodeType.ERROR, "AI_001", "AI 서버가 응답하지 않습니다.", HttpStatus.SERVICE_UNAVAILABLE); + + + private final ResponseCodeType type; private final String code; private final String message;