177 lines
7.3 KiB
Kotlin
177 lines
7.3 KiB
Kotlin
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<CIOEngineConfig>(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<CorpInfo>()
|
|
// "기업명: ${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<DartFinancialResponse>()
|
|
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<String, Double> {
|
|
val result = mutableMapOf<String, Double>()
|
|
// 정규식 설명: 항목명 뒤의 (당기/전기) 괄호 안의 숫자와 콤마를 찾아 숫자로 변환
|
|
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
|
|
) |