package network import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.cio.CIO import io.ktor.client.engine.cio.CIOEngineConfig import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.DEFAULT import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.parameter import io.ktor.http.ContentType.Application.Json import io.ktor.http.Url import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import model.DartFinancialResponse import model.NaverNewsResponse import service.DynamicNewsScraper import service.SafeScraper import service.UrlCacheManager import kotlin.Double object NewsService { private val client = HttpClient(CIO) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } install(Logging) { logger = Logger.DEFAULT level = LogLevel.NONE } } suspend fun fetchAndIngestNews(corpInfo: CorpInfo) { val clientId = "CqXQXHO3h0kqtYsXkePY" // 설정에서 가져오도록 수정 필요 val clientSecret = "DODCxb1M4Z" val qlistNews = listOf( "${corpInfo.stockName} 주가", "${corpInfo.stockName} 실적", "${corpInfo.stockName} 공시", // "${corpInfo.stockName} 이벤트" ) val qlistCorpTrend = listOf( "${corpInfo.cName} 최근 동향", "${corpInfo.cName} 이슈", // "${corpInfo.cName} 투자", // "${corpInfo.cName} 실적" ) (qlistNews + qlistCorpTrend).forEach { query -> try { val response: NaverNewsResponse = client.get("https://openapi.naver.com/v1/search/news.json") { parameter("query", query) parameter("display", 4) // 최근 10개 뉴스 parameter("sort", "date") // 유사도 순 (또는 date 발간순) header("X-Naver-Client-Id", clientId) header("X-Naver-Client-Secret", clientSecret) }.body() SafeScraper.scrapeParallel(corpInfo,response.items.sortedBy { it.pubDate }.distinctBy { Url(it.originallink).host }.take(2) ) } catch (e: Exception) { println("❌ 뉴스 가져오기 실패: ${e.message}") } } } // suspend fun fetchCorpInfo(corpCode: String): String { // val apiKey = "61143d2af0759f6c28ce372d9e339d1e01687abc" // val url = "https://opendart.fss.or.kr/api/company.json?crtfc_key=$apiKey&corp_code=$corpCode" // // return try { // val response = client.get(url).body() // "기업명: ${response.corp_name}, 주요사업: ${response.main_business}" // } catch (e: Exception) { // "기업 정보 로드 실패" // } // } suspend fun fetchFinancialGrowth(corpCode: String?): String { if (corpCode != null) { val apiKey = "61143d2af0759f6c28ce372d9e339d1e01687abc" // 단일회사 주요계정 API (재무상태표, 손익계산서 주요 항목) val url = "https://opendart.fss.or.kr/api/fnlttSinglAcnt.json?crtfc_key=$apiKey&corp_code=$corpCode&bsns_year=2024&reprt_code=11011" return try { val response = client.get(url).body() val accounts = response.list ?: return "재무 데이터 없음" var buffer : StringBuffer = StringBuffer() buffer.append("[재무 분석 데이터]").append("\n") response.list.forEach { it buffer.append("${it.account_nm} (당기)${it?.thstrm_amount}, (전기)${it?.frmtrm_amount}").append("\n") } return buffer.toString() } catch (e: Exception) { "재무 API 연동 실패: ${e.message}" } } else { return "" } } } object FinancialMapper { /** * 제공된 텍스트 데이터를 파싱하여 FinancialStatement 객체로 변환 */ fun mapRawTextToStatement(rawText: String): FinancialStatement { if (rawText.isBlank()) { return FinancialStatement() } val currentValues = extractYearlyValues(rawText, "당기") val previousValues = extractYearlyValues(rawText, "전기") // 1. 영업이익 증가율: (당기 - 전기) / |전기| * 100 val opCurrent = currentValues["영업이익"] ?: 0.0 val opPrevious = previousValues["영업이익"] ?: 0.0 val opGrowth = if (opPrevious != 0.0) ((opCurrent - opPrevious) / Math.abs(opPrevious)) * 100 else 0.0 // 2. 당기순이익 증가율 val niCurrent = currentValues["당기순이익(손실)"] ?: 0.0 val niPrevious = previousValues["당기순이익(손실)"] ?: 0.0 val niGrowth = if (niPrevious != 0.0) ((niCurrent - niPrevious) / Math.abs(niPrevious)) * 100 else 0.0 // 3. ROE: 당기순이익 / 당기 자본총계 * 100 val equityCurrent = currentValues["자본총계"] ?: 1.0 val roe = (niCurrent / equityCurrent) * 100 // 4. 부채비율: 당기 부채총계 / 당기 자본총계 * 100 val debtCurrent = currentValues["부채총계"] ?: 0.0 val debtRatio = (debtCurrent / equityCurrent) * 100 // 5. 당좌비율(유동성): 당기 유동자산 / 당기 유동부채 * 100 val currentAssets = currentValues["유동자산"] ?: 0.0 val currentLiabilities = currentValues["유동부채"] ?: 1.0 val quickRatio = (currentAssets / currentLiabilities) * 100 return FinancialStatement( operatingProfitGrowth = opGrowth, netIncomeGrowth = niGrowth, roe = roe, debtRatio = debtRatio, quickRatio = quickRatio, isOperatingProfitPositive = opCurrent > 0, isNetIncomePositive = niCurrent > 0 ) } private fun extractYearlyValues(text: String, type: String): Map { val result = mutableMapOf() // 정규식 설명: 항목명 뒤의 (당기/전기) 괄호 안의 숫자와 콤마를 찾아 숫자로 변환 val regex = Regex("""([가-힣\s()]+)\s\(?$type\)?([-0-9,.]+)""") regex.findAll(text).forEach { match -> val key = match.groupValues[1].trim() val value = match.groupValues[2].replace(",", "").toDoubleOrNull() ?: 0.0 result[key] = value } return result } } @Serializable data class FinancialStatement( val revenueGrowth: Double = 0.0, // 매출액 증가율 val operatingProfitGrowth: Double = 0.0, // 영업이익 증가율 val netIncomeGrowth: Double = 0.0, // 당기순이익 증가율 val roe: Double = 0.0, // ROE val debtRatio: Double = 0.0, // 부채비율 val quickRatio: Double = 0.0, // 당좌비율 val isOperatingProfitPositive: Boolean = false, // 당기 영업이익 흑자 여부 val isNetIncomePositive: Boolean = false )