Jsoup + Open AI

2025. 2. 21. 19:17TIL

사용자로부터 til keyword를 얻어 db에 리스트 형태로 저장하기 위한 db 설계 til keyword를 얻어 db에 리스트 형태로 저장하기 위한 db 설계

package hjp.nextil.domain.til.entity

import hjp.nextil.domain.member.entity.MemberEntity
import jakarta.persistence.*

@Entity
class TilEntity(

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? =null,

    @ManyToOne
    @JoinColumn(name = "member_id", nullable = false)
    val memberId: MemberEntity,

    @ElementCollection
    @CollectionTable(name = "til_keywords", joinColumns = [JoinColumn(name = "til_id")])
    @Column(name = "keyword")
    var tilKeyword: List<String> = mutableListOf()

) {
}

 

 

사용자가 보낸 URL의  HTML을 가져오기 위해서 Jsoup 라이브러리 사용 

dependencies {
    implementation("org.jsoup:jsoup:1.15.3")
}

 

사용자가 url을 입력하면 해당 url에서 html파일을 가져오는 로직 구현 

package hjp.nextil.domain.til.service

import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.springframework.stereotype.Service


@Service
class TilService {

    fun getBodyText(url: String): String{

        return extractMainContent(fetchHtmlFromUrl(url))

    }

    fun fetchHtmlFromUrl(url: String): Document {

        val cleanUrl = url.replace("\"", "").replace("'", "").trim()
        return Jsoup.connect(cleanUrl)
            .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36") // 크롤링 방지 우회
            .timeout(5000) 
            .get()
    }

    fun extractMainContent(document: Document): String {
        return document.select("body").text()
    }

}

 

 

크롤링을 통해서 html 파일을 가져왔으니 해당 html 본문 내용을 읽고 키워드를 1개에서 3개 사이로 추리는 작업이 시행되어야 한다.

 

Open AI 

 

API KEY 생성

https://platform.openai.com/api-keys

 

 

Open AI 의존성 추가 

dependencies {
    implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M6")
}

 

application.yml 추가

  ai:
    openai:
      api-key: sk-로 시작하는 내가 발급받은 API키
      chat:
        options:
          model: gpt-4o-mini-2024-07-18

 

openAIService 작성

package hjp.nextil.domain.ai

import hjp.nextil.domain.til.entity.TilEntity
import hjp.nextil.domain.til.repository.TilRepository
import hjp.nextil.security.jwt.UserPrincipal
import jakarta.transaction.Transactional
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.chat.messages.SystemMessage
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.stereotype.Service
import kotlinx.serialization.json.Json


@Service
class OpenAIService(
    private val chatClient: ChatClient,
    private val tilRepository: TilRepository,
) {
    @Transactional
    fun extractKeywordsFromText(text: String, user: UserPrincipal): List<String> {

        val systemMessage = SystemMessage("너는 키워드 추출 전문가야. 다음 텍스트의 핵심 개발 키워드를 1~3개 추출해서 JSON 배열로 반환해줘.")
        val userMessage = UserMessage("본문:\n$text")

        // OpenAI API 호출
        val response = chatClient.prompt(Prompt(listOf(systemMessage, userMessage))).call()

        val cleanedJson = response.content()?.removeSurrounding("```json", "```")?.trim() ?: ""

        val newKeywords = parseJsonKeywords(cleanedJson)
        val existingTilEntity = tilRepository.findByMemberId(user.memberId)

        val myKeywords = if (existingTilEntity == null) {
            val newTilEntity = TilEntity(
                memberId = user.memberId,
                tilKeyword = newKeywords
            )
            tilRepository.save(newTilEntity)
            newKeywords.toMutableList()
        } else {
            existingTilEntity.flatMap { it.tilKeyword }.toMutableList()
        }
        tilRepository.deleteAllByMemberId(user.memberId)


        val updatedKeywords = (myKeywords + newKeywords).toSet().toList()

        tilRepository.save(
            TilEntity(
                memberId = user.memberId,
                tilKeyword = updatedKeywords,
            )
        )

        return parseJsonKeywords(cleanedJson)
    }

    private fun parseJsonKeywords(json: String): List<String> {

        return Json.decodeFromString<List<String>>(json)
    }
}

 

Open AI Controller 작성

package hjp.nextil.domain.ai

import hjp.nextil.security.jwt.UserPrincipal
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/openai")
class OpenAIController(
    private val openAIService: OpenAIService,
) {
    @PostMapping("/extract")
    fun extract(@RequestBody text: UserText, @AuthenticationPrincipal user: UserPrincipal): ResponseEntity<List<String>> {
        return ResponseEntity.ok().body(openAIService.extractKeywordsFromText(text = text.tilText, user= user))
    }
}

 

 

테스트: 

테스트 영상: https://www.youtube.com/watch?v=ihcNmnUpkRg