From b35558f43d6a96f5c4f67cf0dac15d8b7363666a Mon Sep 17 00:00:00 2001 From: Jihun-Hwang Date: Tue, 17 Dec 2024 21:40:10 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EB=A6=B0=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/generator/client/OpenAiClient.kt | 6 +- .../few/domain/generator/config/GsonConfig.kt | 6 +- .../config/OpenAiFeignConfiguration.kt | 5 +- .../com/few/domain/generator/core/ChatGpt.kt | 36 ++--- .../few/domain/generator/core/Extractor.kt | 41 +++--- .../generator/core/GroupNewsSummarizer.kt | 12 +- .../domain/generator/core/NaverNewsCrawler.kt | 66 +++++---- .../few/domain/generator/core/NewsGrouper.kt | 16 ++- .../domain/generator/core/PromptGenerator.kt | 130 +++++++++--------- .../domain/generator/core/model/GroupNews.kt | 32 ++--- .../few/domain/generator/core/model/News.kt | 14 +- .../usecase/ExecuteCrawlerUseCase.kt | 67 ++++----- 12 files changed, 232 insertions(+), 199 deletions(-) diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/client/OpenAiClient.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/client/OpenAiClient.kt index be3dc91a5..274d7be5a 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/client/OpenAiClient.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/client/OpenAiClient.kt @@ -10,9 +10,11 @@ import org.springframework.web.bind.annotation.RequestBody @FeignClient( name = "openAiClient", url = "\${openai.api.url}", - configuration = [OpenAiFeignConfiguration::class] + configuration = [OpenAiFeignConfiguration::class], ) interface OpenAiClient { @PostMapping - fun send(@RequestBody request: OpenAiRequest): OpenAiResponse + fun send( + @RequestBody request: OpenAiRequest, + ): OpenAiResponse } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/config/GsonConfig.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/config/GsonConfig.kt index c87938a4c..c25b33ae8 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/config/GsonConfig.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/config/GsonConfig.kt @@ -7,13 +7,11 @@ import org.springframework.context.annotation.Configuration @Configuration class GsonConfig { - @Bean - fun fewGson(): Gson { - return GsonBuilder() + fun fewGson(): Gson = + GsonBuilder() .setLenient() .disableHtmlEscaping() .setPrettyPrinting() .create() - } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/config/OpenAiFeignConfiguration.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/config/OpenAiFeignConfiguration.kt index 56e2b9c8f..ee05968b6 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/config/OpenAiFeignConfiguration.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/config/OpenAiFeignConfiguration.kt @@ -10,10 +10,9 @@ class OpenAiFeignConfiguration( @Value("\${openai.api.key}") private val apiKey: String, ) { @Bean - fun requestInterceptor(): RequestInterceptor { - return RequestInterceptor { template -> + fun requestInterceptor(): RequestInterceptor = + RequestInterceptor { template -> template.header("Authorization", "Bearer $apiKey") template.header("Content-Type", "application/json") } - } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/ChatGpt.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/ChatGpt.kt index a1858dab2..0a3c20117 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/ChatGpt.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/ChatGpt.kt @@ -17,31 +17,35 @@ class ChatGpt( @Value("\${openai.api.model.basic}") private val AI_BASIC_MODEL: String, @Value("\${openai.api.model.advanced}") private val AI_ADVANCED_MODEL: String, ) { + fun summarizeNews(news: News): JsonObject = doAsk(promptGenerator.createSummaryPrompt(news), AI_BASIC_MODEL) - fun summarizeNews(news: News): JsonObject = - doAsk(promptGenerator.createSummaryPrompt(news), AI_BASIC_MODEL) + fun groupNews(newsList: List): JsonObject = doAsk(promptGenerator.createGroupingPrompt(newsList), AI_ADVANCED_MODEL) - fun groupNews(newsList: List): JsonObject = - doAsk(promptGenerator.createGroupingPrompt(newsList), AI_ADVANCED_MODEL) + fun summarizeNewsGroup(group: GroupNews): JsonObject = doAsk(promptGenerator.createSummaryPrompt(group), AI_BASIC_MODEL) - fun summarizeNewsGroup(group: GroupNews): JsonObject = - doAsk(promptGenerator.createSummaryPrompt(group), AI_BASIC_MODEL) - - fun refineSummarizedNewsGroup(group: GroupNews): JsonObject = - doAsk(promptGenerator.createRefinePrompt(group), AI_BASIC_MODEL) + fun refineSummarizedNewsGroup(group: GroupNews): JsonObject = doAsk(promptGenerator.createRefinePrompt(group), AI_BASIC_MODEL) /** * 공통된 OpenAI 요청 처리 및 JSON 결과 반환 */ - private fun doAsk(prompt: List>, aiModel: String): JsonObject { - val request = OpenAiRequest( - model = aiModel, - messages = prompt - ) + private fun doAsk( + prompt: List>, + aiModel: String, + ): JsonObject { + val request = + OpenAiRequest( + model = aiModel, + messages = prompt, + ) val response = openAiClient.send(request) - val resultContent = response.choices.firstOrNull()?.message?.content?.trim() - ?: throw Exception("요약 결과를 찾을 수 없습니다.") + val resultContent = + response.choices + .firstOrNull() + ?.message + ?.content + ?.trim() + ?: throw Exception("요약 결과를 찾을 수 없습니다.") return fewGson.fromJson(resultContent, JsonObject::class.java) } diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/Extractor.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/Extractor.kt index 1c47b4d61..c90bd6a4d 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/Extractor.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/Extractor.kt @@ -2,7 +2,6 @@ package com.few.domain.generator.core import com.few.domain.generator.core.model.News import com.google.gson.Gson -import org.springframework.stereotype.Component import com.google.gson.reflect.TypeToken import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope @@ -11,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import org.springframework.stereotype.Component import java.io.File @Component @@ -18,7 +18,6 @@ class Extractor( private val chatGpt: ChatGpt, private val fewGson: Gson, ) { - private val log = KotlinLogging.logger {} fun loadContentFromJson(inputFilePath: String): List { @@ -33,28 +32,33 @@ class Extractor( return fewGson.fromJson(file.readText(), type) } - suspend fun extractAndSaveNews(inputFilePath: String, outputFilePath: String): Int { + suspend fun extractAndSaveNews( + inputFilePath: String, + outputFilePath: String, + ): Int { val newsModels = loadContentFromJson(inputFilePath) val semaphore = Semaphore(5) // 최대 동시 실행 개수 제한 val routines = mutableListOf>() for (newsModel in newsModels) { - val routine = CoroutineScope(Dispatchers.IO).async { - semaphore.withPermit { - try { - val summarizedNews = chatGpt.summarizeNews(newsModel) - newsModel.summary = summarizedNews.get("summary")?.asString ?: "요약을 생성할 수 없습니다." - newsModel.importantSentences = if (summarizedNews.has("important_sentences")) { - val sentencesJsonArray = summarizedNews.getAsJsonArray("important_sentences") - sentencesJsonArray.mapNotNull { it.asString } - } else { - emptyList() + val routine = + CoroutineScope(Dispatchers.IO).async { + semaphore.withPermit { + try { + val summarizedNews = chatGpt.summarizeNews(newsModel) + newsModel.summary = summarizedNews.get("summary")?.asString ?: "요약을 생성할 수 없습니다." + newsModel.importantSentences = + if (summarizedNews.has("important_sentences")) { + val sentencesJsonArray = summarizedNews.getAsJsonArray("important_sentences") + sentencesJsonArray.mapNotNull { it.asString } + } else { + emptyList() + } + } catch (e: Exception) { + log.error { "${newsModel.title}에 대한 요약 중 오류 발생: ${e.message}" } } - } catch (e: Exception) { - log.error { "${newsModel.title}에 대한 요약 중 오류 발생: ${e.message}" } } } - } routines.add(routine) } @@ -68,7 +72,10 @@ class Extractor( return newsModels.size } - fun saveNewsToJson(newsList: List, outputFilePath: String) { + fun saveNewsToJson( + newsList: List, + outputFilePath: String, + ) { // List을 JSON 문자열로 변환 val newsData = newsList.map { it.toMap() } val jsonString = fewGson.toJson(newsData) diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/GroupNewsSummarizer.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/GroupNewsSummarizer.kt index db28d1ae0..3fd509a9b 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/GroupNewsSummarizer.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/GroupNewsSummarizer.kt @@ -15,7 +15,10 @@ class GroupNewsSummarizer( ) { private val log = KotlinLogging.logger {} - fun summarizeAndSaveGroupedNews(inputFilePath: String, outputFilePath: String): Int { + fun summarizeAndSaveGroupedNews( + inputFilePath: String, + outputFilePath: String, + ): Int { val groupedNews = loadGroupedNews(inputFilePath) for ((index, group) in groupedNews.withIndex()) { @@ -53,14 +56,17 @@ class GroupNewsSummarizer( return fewGson.fromJson(sectionData, SectionContent::class.java) } - private fun saveSummariesToJson(groupedNews: List, outputFilePath: String) { + private fun saveSummariesToJson( + groupedNews: List, + outputFilePath: String, + ) { // GroupNewsModel 객체 리스트를 Map 리스트로 변환 val groupNewsData = groupedNews.map { it.toMap() } // JSON 파일로 저장 File(outputFilePath).writeText( fewGson.toJson(groupNewsData), - Charsets.UTF_8 + Charsets.UTF_8, ) } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/NaverNewsCrawler.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/NaverNewsCrawler.kt index 1bde98c56..b6c782c7a 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/NaverNewsCrawler.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/NaverNewsCrawler.kt @@ -6,10 +6,10 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.springframework.stereotype.Component -import java.util.regex.Pattern import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter +import java.util.regex.Pattern @Component class NaverNewsCrawler( @@ -20,7 +20,10 @@ class NaverNewsCrawler( private val log = KotlinLogging.logger {} private val regex_news_links = "https://n\\.news\\.naver\\.com/mnews/article/\\d+/\\d+$" private val headers = - mapOf("User-Agent" to "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36") + mapOf( + "User-Agent" to + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", + ) private fun getSoup(url: String): Document { val connection = Jsoup.connect(url) @@ -30,8 +33,10 @@ class NaverNewsCrawler( return connection.get() } - private fun makeUrl(sid: Int, page: Int) = - "https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=$sid#&date=%2000:00:00&page=$page" + private fun makeUrl( + sid: Int, + page: Int, + ) = "https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=$sid#&date=%2000:00:00&page=$page" fun getNaverNewsUrls(sid: Int): List { log.info { "$sid 분야의 뉴스 링크를 수집합니다." } @@ -43,10 +48,11 @@ class NaverNewsCrawler( // Regex to match the desired link pattern val pattern = Pattern.compile(regex_news_links) - val links = soup.select("a[href]").mapNotNull { element -> - val href = element.attr("href") - if (pattern.matcher(href).matches()) href else null - } + val links = + soup.select("a[href]").mapNotNull { element -> + val href = element.attr("href") + if (pattern.matcher(href).matches()) href else null + } allLinks.addAll(links) @@ -66,10 +72,14 @@ class NaverNewsCrawler( val title = soup.selectFirst("#title_area > span") val date = - soup.selectFirst("#ct > div.media_end_head.go_trans > div.media_end_head_info.nv_notrans > div.media_end_head_info_datestamp > div:nth-child(1) > span") + soup.selectFirst( + "#ct > div.media_end_head.go_trans > div.media_end_head_info.nv_notrans > div.media_end_head_info_datestamp > div:nth-child(1) > span", + ) val content = soup.selectFirst("#dic_area") val linkElement = - soup.selectFirst("#ct > div.media_end_head.go_trans > div.media_end_head_info.nv_notrans > div.media_end_head_info_datestamp > a.media_end_head_origin_link") + soup.selectFirst( + "#ct > div.media_end_head.go_trans > div.media_end_head_info.nv_notrans > div.media_end_head_info_datestamp > a.media_end_head_origin_link", + ) val originalLink = linkElement?.attr("href") // TODO 원본 데이터 DB 저장으로 변경 @@ -82,31 +92,33 @@ class NaverNewsCrawler( val dateStr = date.text().trim() val dateParts = dateStr.split(" ") - val dateTime: LocalDateTime = if (dateParts.size == 3) { - val dateOnly = dateParts[0] - val amPm = dateParts[1] - val time = dateParts[2] - - val (hour, minute) = time.split(":").map { it.toInt() } - val adjustedHour = when { - amPm == "오후" && hour != 12 -> hour + 12 - amPm == "오전" && hour == 12 -> 0 - else -> hour + val dateTime: LocalDateTime = + if (dateParts.size == 3) { + val dateOnly = dateParts[0] + val amPm = dateParts[1] + val time = dateParts[2] + + val (hour, minute) = time.split(":").map { it.toInt() } + val adjustedHour = + when { + amPm == "오후" && hour != 12 -> hour + 12 + amPm == "오전" && hour == 12 -> 0 + else -> hour + } + + val dateTimeStr = "$dateOnly ${"%02d".format(adjustedHour)}:${"%02d".format(minute)}" + LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy.MM.dd. HH:mm")) + } else { + LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy.MM.dd. HH:mm")) } - val dateTimeStr = "$dateOnly ${"%02d".format(adjustedHour)}:${"%02d".format(minute)}" - LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy.MM.dd. HH:mm")) - } else { - LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy.MM.dd. HH:mm")) - } - return originalLink?.let { News( title = title.text().trim(), content = content.text().trim(), date = dateTime, link = url, - originalLink = it + originalLink = it, ) } } diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/NewsGrouper.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/NewsGrouper.kt index 11d5ee5b6..f0e2e4777 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/NewsGrouper.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/NewsGrouper.kt @@ -2,12 +2,12 @@ package com.few.domain.generator.core import com.few.domain.generator.core.model.GroupNews import com.few.domain.generator.core.model.News -import java.io.File import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.reflect.TypeToken import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Component +import java.io.File @Component class NewsGrouper( @@ -16,7 +16,10 @@ class NewsGrouper( ) { private val log = KotlinLogging.logger {} - fun groupAndSaveNews(inputFilePath: String, outputFilePath: String) { + fun groupAndSaveNews( + inputFilePath: String, + outputFilePath: String, + ) { val newsList = loadSummarizedNews(inputFilePath) log.info { "뉴스 그룹화 진행 중..." } @@ -62,10 +65,11 @@ class NewsGrouper( // 뉴스가 3개 이상인 경우만 추가 if (newsInGroup.size >= 3) { - val groupNews = GroupNews( - topic = group.getAsJsonPrimitive("topic").asString, - news = newsInGroup - ) + val groupNews = + GroupNews( + topic = group.getAsJsonPrimitive("topic").asString, + news = newsInGroup, + ) result.add(groupNews) log.info { "groupNewsIds: $groupNewsIds" } } diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/PromptGenerator.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/PromptGenerator.kt index 043623d63..459ea56bd 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/PromptGenerator.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/PromptGenerator.kt @@ -9,7 +9,8 @@ import org.springframework.stereotype.Component class PromptGenerator( private val fewGson: Gson, ) { - private val jsonTemplate: String = """ + private val jsonTemplate: String = + """ { "section": { "title": "섹션 제목", // 문장 형태로 작성해야 합니다. @@ -25,59 +26,62 @@ class PromptGenerator( ] } } - """.trimIndent() + """.trimIndent() fun createSummaryPrompt(group: GroupNews): List> { // 뉴스 요약 문자열 생성 - val newsSummaries = group.news.joinToString("\n") { news -> - val importantSentences = news.importantSentences.joinToString("\n") { sentence -> " - $sentence" } - """ - - ID: ${news.id} - 제목: ${news.title} - 요약: ${news.summary} - 중요한 문장: - $importantSentences - """.trimIndent() - } + val newsSummaries = + group.news.joinToString("\n") { news -> + val importantSentences = news.importantSentences.joinToString("\n") { sentence -> " - $sentence" } + """ + - ID: ${news.id} + 제목: ${news.title} + 요약: ${news.summary} + 중요한 문장: + $importantSentences + """.trimIndent() + } // 프롬프트 명령어 생성 - val command = """ - # 지침 - - 당신은 뉴스 요약 전문 작가입니다. - - 여러 뉴스를 종합하여 블로그 포스팅 스타일 요약을 작성하는 것이 특기입니다. - - 항상 정확한 출처 표시와 함께 정보를 제공합니다. - - 응답은 반드시 JSON 형식이어야 합니다. - - # 제약사항 - - body에서 '중요한 문장'을 참고하여 작성한 경우, 반드시 해당 뉴스의 ID를 사용하여 표시해야 합니다. - 형식: '[sentence](ID)' - 예시: - - 원본 중요한 문장: "삼성전자가 새로운 스마트폰을 출시했다." - - ID: NEWS001 - - 작성된 문장: [삼성전자가 혁신적인 기능을 탑재한 새로운 스마트폰을 선보였습니다.](NEWS001) - - 'sentence'을 그대로 사용하지 말아주세요. - - 모든 중요한 문장은 위와 같은 방식과 형식으로 변형되어 사용되어야 합니다. 그대로 사용하지 말아주세요. - - # 입력문 - 다음은 "${group.topic}" 주제에 관한 여러 뉴스 기사의 요약입니다: - - $newsSummaries - - 이 뉴스들을 종합하여 섹션별로 구성된 JSON 형식의 요약을 작성해주세요. - - # 출력 JSON 형식 - $jsonTemplate - """.trimIndent() + val command = + """ + # 지침 + - 당신은 뉴스 요약 전문 작가입니다. + - 여러 뉴스를 종합하여 블로그 포스팅 스타일 요약을 작성하는 것이 특기입니다. + - 항상 정확한 출처 표시와 함께 정보를 제공합니다. + - 응답은 반드시 JSON 형식이어야 합니다. + + # 제약사항 + - body에서 '중요한 문장'을 참고하여 작성한 경우, 반드시 해당 뉴스의 ID를 사용하여 표시해야 합니다. + 형식: '[sentence](ID)' + 예시: + - 원본 중요한 문장: "삼성전자가 새로운 스마트폰을 출시했다." + - ID: NEWS001 + - 작성된 문장: [삼성전자가 혁신적인 기능을 탑재한 새로운 스마트폰을 선보였습니다.](NEWS001) + - 'sentence'을 그대로 사용하지 말아주세요. + - 모든 중요한 문장은 위와 같은 방식과 형식으로 변형되어 사용되어야 합니다. 그대로 사용하지 말아주세요. + + # 입력문 + 다음은 "${group.topic}" 주제에 관한 여러 뉴스 기사의 요약입니다: + + $newsSummaries + + 이 뉴스들을 종합하여 섹션별로 구성된 JSON 형식의 요약을 작성해주세요. + + # 출력 JSON 형식 + $jsonTemplate + """.trimIndent() // 프롬프트 반환 return listOf( mapOf("role" to "system", "content" to "당신은 뉴스 요약 전문 작가입니다. 여러 뉴스를 종합하여 블로그 포스팅 스타일의 요약을 작성하는 것이 특기입니다."), - mapOf("role" to "user", "content" to command) + mapOf("role" to "user", "content" to command), ) } fun createSummaryPrompt(news: News): List> { - val command = """ + val command = + """ 다음 뉴스 기사를 분석하고 요약해주세요: 제목: ${news.title} @@ -105,11 +109,11 @@ class PromptGenerator( - 중요한 문장은 반드시 본문에서 그대로 추출해야 합니다. 수정하거나 재작성하지 마세요. - 키워드는 최대 3개, 본문에서 나온 단어로 작성해주세요. - 중요한 문장은 정확히 3개를 선택해야 합니다. - """.trimIndent() + """.trimIndent() return listOf( mapOf("role" to "system", "content" to "당신은 뉴스 기사를 간결하게 요약하는 전문가입니다. 주어진 뉴스 기사를 분석하고 요약해야 합니다."), - mapOf("role" to "user", "content" to command) + mapOf("role" to "user", "content" to command), ) } @@ -118,32 +122,34 @@ class PromptGenerator( val groupJson = fewGson.toJson(group.toMap()) // JSON 문자열로 변환 // 프롬프트 명령어 생성 - val command = """ - # 지침 - - 주어진 블로그 포스트 스타일의 요약을 더 친절하고 매끄럽게 수정해주세요. - - 말투를 더 부드럽고 친근하게 바꿔주세요. - - 문장 구조를 자연스럽게 다듬어주세요. - - 내용의 정확성은 유지하면서 가독성을 높여주세요. - - 형식: '[sentence](ID)'은 그대로 유지해야 합니다. '[', ']', '(', ')' 은 제거하지 않습니다. 말투만 수정되어야 합니다. - - 응답은 반드시 JSON 형식이어야 합니다. - - # 입력문 - $groupJson - - # 출력 JSON 형식 - $jsonTemplate - """.trimIndent() + val command = + """ + # 지침 + - 주어진 블로그 포스트 스타일의 요약을 더 친절하고 매끄럽게 수정해주세요. + - 말투를 더 부드럽고 친근하게 바꿔주세요. + - 문장 구조를 자연스럽게 다듬어주세요. + - 내용의 정확성은 유지하면서 가독성을 높여주세요. + - 형식: '[sentence](ID)'은 그대로 유지해야 합니다. '[', ']', '(', ')' 은 제거하지 않습니다. 말투만 수정되어야 합니다. + - 응답은 반드시 JSON 형식이어야 합니다. + + # 입력문 + $groupJson + + # 출력 JSON 형식 + $jsonTemplate + """.trimIndent() // 프롬프트 반환 return listOf( mapOf("role" to "system", "content" to "당신은 친절하고 매끄러운 글쓰기에 능숙한 편집자입니다."), - mapOf("role" to "user", "content" to command) + mapOf("role" to "user", "content" to command), ) } fun createGroupingPrompt(newsList: List): List> { val newsSummaries = newsList.joinToString("\n") { "${it.id}: ${it.summary}" } - val command = """ + val command = + """ 다음은 여러 뉴스 기사의 요약입니다. 이 뉴스들을 비슷한 주제끼리 그룹핑해주세요: $newsSummaries @@ -172,11 +178,11 @@ class PromptGenerator( - 각 그룹의 "news_ids"는 위 목록의 뉴스 ID를 나타냅니다. - 그룹의 수에는 제한이 없지만, 너무 많은 그룹을 만들지 않도록 주의하세요. 뉴스 총 개수의 10% 이상의 그룹을 만들지 않도록 주의하세요. - 그룹의 주제는 추상적인 문장을 사용하지 말고 구체적인 문장으로 작성해주세요. - """.trimIndent() + """.trimIndent() return listOf( // TODO 클래스 정의 mapOf("role" to "system", "content" to "당신은 뉴스 기사를 주제별로 그룹핑하는 전문가입니다. 주어진 뉴스 요약들을 분석하고 비슷한 주제끼리 그룹화해야 합니다."), - mapOf("role" to "user", "content" to command) + mapOf("role" to "user", "content" to command), ) } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/GroupNews.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/GroupNews.kt index d90e84094..3e47d7664 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/GroupNews.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/GroupNews.kt @@ -5,13 +5,12 @@ data class GroupNews( val news: List = listOf(), var section: SectionContent = SectionContent(), ) { - fun toMap(): Map { - return mapOf( + fun toMap(): Map = + mapOf( "topic" to topic, "news" to news.map { it.toMap() }, - "section" to section.toDict() + "section" to section.toDict(), ) - } companion object { fun fromMap(data: Map): GroupNews { @@ -20,7 +19,7 @@ data class GroupNews( return GroupNews( topic = data["topic"] as String, news = newsList, - section = sectionData + section = sectionData, ) } } @@ -30,12 +29,11 @@ data class SectionContent( val title: String = "", val contents: List = listOf(), ) { - fun toDict(): Map { - return mapOf( + fun toDict(): Map = + mapOf( "title" to title, - "contents" to contents.map { it.toDict() } + "contents" to contents.map { it.toDict() }, ) - } companion object { fun fromDict(data: Map): SectionContent { @@ -43,7 +41,7 @@ data class SectionContent( (data["contents"] as? List>)?.map { Content.fromDict(it) } ?: emptyList() return SectionContent( title = data["title"] as? String ?: "", - contents = contentsList + contents = contentsList, ) } } @@ -53,19 +51,17 @@ data class Content( val subTitle: String = "", val body: String = "", ) { - fun toDict(): Map { - return mapOf( + fun toDict(): Map = + mapOf( "subTitle" to subTitle, - "body" to body + "body" to body, ) - } companion object { - fun fromDict(data: Map): Content { - return Content( + fun fromDict(data: Map): Content = + Content( subTitle = data["subTitle"] as? String ?: "", - body = data["body"] as? String ?: "" + body = data["body"] as? String ?: "", ) - } } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/News.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/News.kt index 731069027..51217ecc9 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/News.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/core/model/News.kt @@ -15,8 +15,8 @@ data class News( var importantSentences: List = emptyList(), var keywords: List = emptyList(), ) { - fun toMap(): Map { - return mapOf( + fun toMap(): Map = + mapOf( "id" to id, "title" to title, "content" to content, @@ -25,13 +25,12 @@ data class News( "summary" to summary, "original_link" to originalLink, "important_sentences" to importantSentences, - "keywords" to keywords + "keywords" to keywords, ) - } companion object { - fun fromMap(data: Map): News { - return News( + fun fromMap(data: Map): News = + News( id = data["id"] as? String ?: UUID.randomUUID().toString(), title = data["title"] as String, content = data["content"] as String, @@ -40,8 +39,7 @@ data class News( summary = data["summary"] as? String ?: "", originalLink = data["original_link"] as? String ?: "", importantSentences = data["important_sentences"] as? List ?: emptyList(), - keywords = data["keywords"] as? List ?: emptyList() + keywords = data["keywords"] as? List ?: emptyList(), ) - } } } \ No newline at end of file diff --git a/domain-generator/src/main/kotlin/com/few/domain/generator/usecase/ExecuteCrawlerUseCase.kt b/domain-generator/src/main/kotlin/com/few/domain/generator/usecase/ExecuteCrawlerUseCase.kt index d9d423dce..16f3196b9 100644 --- a/domain-generator/src/main/kotlin/com/few/domain/generator/usecase/ExecuteCrawlerUseCase.kt +++ b/domain-generator/src/main/kotlin/com/few/domain/generator/usecase/ExecuteCrawlerUseCase.kt @@ -18,43 +18,44 @@ class ExecuteCrawlerUseCase( ) { private val log = KotlinLogging.logger {} - //TODO: @Transactional - fun execute(useCaseIn: ExecuteCrawlerUseCaseIn): ExecuteCrawlerUseCaseOut = runBlocking { - /** - * TODO: 아직 포스팅되지 않은 크롤링 데이터가 있는지 DB에서 확인 - * 있는 경우 조회해서 리턴 - * 없는 경우 크롤링 시작 - */ - - // 1. 네이버 뉴스 크롤링 - log.info { "크롤링이 시작" } - val newsUrls = crawler.getNaverNewsUrls(useCaseIn.sid) - - val results = mutableListOf() - for ((i, url) in newsUrls.withIndex()) { - val newsData = crawler.getNewsContent(url) - if (newsData != null) { - results.add(newsData) + // TODO: @Transactional + fun execute(useCaseIn: ExecuteCrawlerUseCaseIn): ExecuteCrawlerUseCaseOut = + runBlocking { + /** + * TODO: 아직 포스팅되지 않은 크롤링 데이터가 있는지 DB에서 확인 + * 있는 경우 조회해서 리턴 + * 없는 경우 크롤링 시작 + */ + + // 1. 네이버 뉴스 크롤링 + log.info { "크롤링이 시작" } + val newsUrls = crawler.getNaverNewsUrls(useCaseIn.sid) + + val results = mutableListOf() + for ((i, url) in newsUrls.withIndex()) { + val newsData = crawler.getNewsContent(url) + if (newsData != null) { + results.add(newsData) + } + log.info { "뉴스 ${i + 1}/${newsUrls.size} 처리 완료" } + Thread.sleep(1000) // 1초 딜레이 } - log.info { "뉴스 ${i + 1}/${newsUrls.size} 처리 완료" } - Thread.sleep(1000) // 1초 딜레이 - } - crawler.saveContentAsJson(results) - log.info { "크롤링이 완료 및 요약 시작" } + crawler.saveContentAsJson(results) + log.info { "크롤링이 완료 및 요약 시작" } - // 2. 뉴스 추출 및 요약 - extractor.extractAndSaveNews("crawled_news.json", "extracted_news.json") + // 2. 뉴스 추출 및 요약 + extractor.extractAndSaveNews("crawled_news.json", "extracted_news.json") - // 3. 뉴스 그룹화 - grouper.groupAndSaveNews("extracted_news.json", "grouped_news.json") + // 3. 뉴스 그룹화 + grouper.groupAndSaveNews("extracted_news.json", "grouped_news.json") - // 4. 그룹 뉴스 요약 - summarizer.summarizeAndSaveGroupedNews("grouped_news.json", "summarized_groups.json") + // 4. 그룹 뉴스 요약 + summarizer.summarizeAndSaveGroupedNews("grouped_news.json", "summarized_groups.json") - ExecuteCrawlerUseCaseOut( - useCaseIn.sid, - listOf(UUID.randomUUID().toString()) // TODO: DB 저장 시 크롤링 고유 ID 응답 - ) - } + ExecuteCrawlerUseCaseOut( + useCaseIn.sid, + listOf(UUID.randomUUID().toString()), // TODO: DB 저장 시 크롤링 고유 ID 응답 + ) + } } \ No newline at end of file