TreeNut-KR / ChatBot

ChatBot 웹사이트 프로젝트
GNU General Public License v3.0
1 stars 0 forks source link

chatroom 기능 구현 계획 #28

Open CutTheWire opened 2 months ago

CutTheWire commented 2 months ago

chatbot(캐릭터 채팅) & office(GPT)의 채팅 생성 방식 설명

도메인은 nono.me라고 가정

CutTheWire commented 2 months ago

기본 설계 방식

  1. JWT 토큰 기반 인증 미들웨어: TokenAuth.kt 파일에서 JWT 토큰을 처리하여 사용자의 user_id를 추출하는 미들웨어를 사용. 이 미들웨어는 각 요청에 대해 유저를 인증하고 user_id를 제공하여 MongoDB 관련 기능과 통합할 수 있도록 처리.

  2. 라우팅 및 URL 구조:

    • Chatbot
      • nono.me/chatroom/c/캐릭터 고유번호 페이지에서 유저가 채팅을 입력하면 FastAPI 서버에 해당 데이터를 전송하여 MongoDB에 chatbot_log_{user_id} 컬렉션을 생성.
      • nono.me/chatroom/c/캐릭터 고유번호/u/채팅방 고유번호 페이지에서 해당 채팅방 데이터를 로드.
    • Office (GPT)
      • nono.me/chatroom/o/gpt 페이지에서 채팅을 입력하면 FastAPI 서버에 해당 데이터를 전송하여 MongoDB에 office_log_{user_id} 컬렉션을 생성.
      • nono.me/chatroom/o/gpt/u/채팅방 고유번호 페이지에서 해당 채팅방 데이터를 로드.

Spring Boot 구성 제안

  1. 컨트롤러 (Controller) 구성

    • Spring Boot에서 사용자가 해당 URL에 접근할 때 필요한 요청을 처리하는 컨트롤러를 정의.
    • FastAPI 서버에 필요한 데이터를 전달하고, MongoDB에서 채팅 방 정보를 가져오거나 업데이트하는 역할을 담당.
    @RestController
    @RequestMapping("/server/chatroom")
    class ChatController(private val tokenAuth: TokenAuth) {
    
       // 채팅방 생성 (Chatbot)
       @PostMapping("/c/{characterId}")
       fun createChatbotRoom(
           @RequestHeader("Authorization") token: String,
           @PathVariable characterId: String
       ): ResponseEntity<String> {
           val userId = tokenAuth.authGuard(token)
           // FastAPI 서버로 채팅방 생성 요청을 보내는 로직
           // ...
           return ResponseEntity.ok("chatroom created with characterId: $characterId")
       }
    
       // 채팅방 생성 (Office GPT)
       @PostMapping("/o/gpt")
       fun createGptRoom(@RequestHeader("Authorization") token: String): ResponseEntity<String> {
           val userId = tokenAuth.authGuard(token)
           // FastAPI 서버로 GPT 채팅방 생성 요청을 보내는 로직
           // ...
           return ResponseEntity.ok("GPT chatroom created")
       }
    
       // 채팅 로드 (Chatbot)
       @GetMapping("/c/{characterId}/u/{chatroomId}")
       fun getChatbotLog(
           @RequestHeader("Authorization") token: String,
           @PathVariable characterId: String,
           @PathVariable chatroomId: String
       ): ResponseEntity<String> {
           val userId = tokenAuth.authGuard(token)
           // FastAPI 서버로부터 MongoDB에서 데이터를 받아오는 로직
           // ...
           return ResponseEntity.ok("chat logs for chatbot")
       }
    
       // 채팅 로드 (Office GPT)
       @GetMapping("/o/gpt/u/{chatroomId}")
       fun getGptLog(
           @RequestHeader("Authorization") token: String,
           @PathVariable chatroomId: String
       ): ResponseEntity<String> {
           val userId = tokenAuth.authGuard(token)
           // FastAPI 서버로부터 MongoDB에서 데이터를 받아오는 로직
           // ...
           return ResponseEntity.ok("chat logs for GPT")
       }
    }
  2. Feign 클라이언트로 FastAPI 서버 호출:

    • Spring Boot에서 FastAPI 서버로 데이터를 주고받기 위해 FeignClient 또는 RestTemplate을 사용할 수 있습니다. FeignClient를 사용하면 간결하게 외부 API를 호출할 수 있습니다.
    @FeignClient(name = "chatService", url = "\${fastapi.server.url}")
    interface ChatServiceClient {
    
       @PostMapping("/mongo/chatbot/create")
       fun createChatbotRoom(@RequestBody userId: Long): ResponseEntity<String>
    
       @PostMapping("/mongo/office/create")
       fun createGptRoom(@RequestBody userId: Long): ResponseEntity<String>
    
       @GetMapping("/mongo/chatbot/load/{chatroomId}")
       fun getChatbotLog(@PathVariable chatroomId: String): ResponseEntity<String>
    
       @GetMapping("/mongo/office/load/{chatroomId}")
       fun getGptLog(@PathVariable chatroomId: String): ResponseEntity<String>
    }

    설명:

    • @FeignClient를 통해 FastAPI 서버와 통신하며, 채팅방 생성, 로그 저장, 로그 불러오기 등의 작업을 FastAPI에 요청.
    • RestTemplate을 사용할 경우 더 세부적인 설정이 필요하지만, 그만큼 유연한 커스터마이징이 가능합니다.
  3. 의존성 주입 및 환경 설정:

    • application.yml 파일에 FastAPI 서버의 URL을 정의합니다.
    fastapi:
     server:
       url: http://localhost:8000  # FastAPI 서버의 주소
  4. 예외 처리 (Custom Exceptions):

    • TokenAuth.kt에서 정의한 JWT 관련 예외들을 전역적으로 처리하는 방식으로 Spring Boot의 @ControllerAdvice를 사용해 처리합니다.
    @ControllerAdvice
    class GlobalExceptionHandler {
    
       @ExceptionHandler(TokenExpiredException::class)
       fun handleTokenExpiredException(ex: TokenExpiredException): ResponseEntity<String> {
           return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.message)
       }
    
       @ExceptionHandler(TokenMalformedException::class)
       fun handleTokenMalformedException(ex: TokenMalformedException): ResponseEntity<String> {
           return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.message)
       }
    
       // 다른 예외 처리 로직도 동일하게 추가
    }
CutTheWire commented 1 month ago

전체적인 코드 개편과 Model 수정으로 인해 위, 이슈의 코드는 사용 중지.

해당 이슈에 대한 코드를 사용.

파일 구조

📦ChatBot_Backend ┣ 📂controller ┃ ┣ 📂config ┃ ┃ ┣ 📜SecurityConfig.kt ┃ ┃ ┗ 📜WebClientConfig.kt ┃ ┣ 📜CharacterController.kt ┃ ┣ 📜ChatroomController.kt ┃ ┗ 📜UserController.kt ┣ 📂exceptions ┃ ┗ 📜TokenAuthException.kt ┣ 📂middleware ┃ ┗ 📜TokenAuth.kt ┣ 📂model ┃ ┣ 📜Character.kt ┃ ┣ 📜Chatroom.kt ┃ ┣ 📜Officeroom.kt ┃ ┗ 📜User.kt ┣ 📂repository ┃ ┣ 📜CharacterRepository.kt ┃ ┣ 📜ChatroomRepository.kt ┃ ┣ 📜OfficeroomRepository.kt ┃ ┗ 📜UserRepository.kt ┣ 📂service ┃ ┣ 📜CharacterService.kt ┃ ┣ 📜ChatroomService.kt ┃ ┗ 📜UserService.kt ┗ 📜ChatBotBackendApplication.kt

WebClientConfig.kt

package com.TreeNut.ChatBot_Backend.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class WebClientConfig {

    @Bean
    fun webClientBuilder(): WebClient.Builder {
        return WebClient.builder()
            .baseUrl("http://fastapi:8000") // 기본 URL 설정
    }
}

ChatroomController.kt

package com.TreeNut.ChatBot_Backend.controller

import com.TreeNut.ChatBot_Backend.service.ChatroomService
import com.TreeNut.ChatBot_Backend.middleware.TokenAuth
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/server/chatroom")
class ChatroomController(
    private val chatroomService: ChatroomService,
    private val tokenAuth: TokenAuth
) {

    @GetMapping("/test")
    fun testRoom(
        @RequestHeader("Authorization") authorization: String?
    ): ResponseEntity<Map<Any, Any>> {
        val token = authorization
            ?: return ResponseEntity.badRequest().body(mapOf("status" to 401, "message" to "토큰 없음"))

        val userId = tokenAuth.authGuard(token)
            ?: return ResponseEntity.badRequest().body(mapOf("status" to 401, "message" to "유효한 토큰이 필요합니다."))

        return ResponseEntity.ok(mapOf("status" to 200, "user_id" to userId))
    }
    @GetMapping("/gpt")
    fun createGptRoom(
        @RequestHeader("Authorization") authorization: String?
    ): Mono<ResponseEntity<Map<String, Any>>> {
        val token = authorization
            ?: return Mono.just(ResponseEntity.badRequest().body(mapOf("status" to 401, "message" to "토큰 없음")))

        val userId = tokenAuth.authGuard(token)
            ?: return Mono.just(ResponseEntity.badRequest().body(mapOf("status" to 401, "message" to "유효한 토큰이 필요합니다.")))

        // FastAPI 서버에 요청하고 결과를 받아 채팅방을 생성하는 로직
        return chatroomService.createOfficeroom(userId)
            .map { response ->
                // 응답을 그대로 반환
                ResponseEntity.ok(mapOf(
                    "status" to 200,
                    "message" to "채팅방이 성공적으로 생성되었습니다.",
                    "chatroom" to response // FastAPI에서 받은 응답
                ))
            }
            .defaultIfEmpty(
                ResponseEntity.status(500).body(mapOf(
                    "status" to 500,
                    "message" to "채팅방 생성에 실패했습니다."
                ))
            )
    }

}

Chatroom.kt

package com.TreeNut.ChatBot_Backend.model

import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
@Table(name = "chatroom")
data class Chatroom(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "idx")
    val idx: Long? = null,

    @Column(name = "userid", nullable = false, length = 100)
    val userid: String, // 외래 키로 설정될 수 있음

    @Column(name = "characters_idx", nullable = false)
    val charactersIdx: Int, // 외래 키로 설정될 수 있음

    @Column(name = "mongo_chatlog", length = 100)
    val mongoChatlog: String? = null,

    @Column(name = "created_at", updatable = false)
    val createdAt: LocalDateTime = LocalDateTime.now(),

    @Column(name = "updated_at")
    var updatedAt: LocalDateTime = LocalDateTime.now()
) {
    @PreUpdate
    fun onUpdate() {
        updatedAt = LocalDateTime.now()
    }
}

Officeroom.kt

package com.TreeNut.ChatBot_Backend.model

import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
@Table(name = "officeroom")
data class Officeroom(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "idx")
    val idx: Long? = null,

    @Column(name = "userid", nullable = false, length = 100)
    val userid: String, // 외래 키로 설정될 수 있음

    @Column(name = "mongo_chatlog", length = 100)
    val mongoChatlog: String? = null,

    @Column(name = "created_at", updatable = false)
    val createdAt: LocalDateTime = LocalDateTime.now(),

    @Column(name = "updated_at")
    var updatedAt: LocalDateTime = LocalDateTime.now()
) {
    @PreUpdate
    fun onUpdate() {
        updatedAt = LocalDateTime.now()
    }
}

ChatroomRepository.kt

package com.TreeNut.ChatBot_Backend.repository

import com.TreeNut.ChatBot_Backend.model.Chatroom
import com.TreeNut.ChatBot_Backend.model.Officeroom
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface ChatroomRepository : JpaRepository<Chatroom, Long> {
    fun findByUserid(userId: String): Chatroom? // 수정된 메소드 이름

}

OfficeroomRepository.kt

package com.TreeNut.ChatBot_Backend.repository

import com.TreeNut.ChatBot_Backend.model.Officeroom
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface OfficeroomRepository : JpaRepository<Officeroom, Long> {
    fun findByUserid(userId: String): Officeroom?
}

ChatroomService.kt

package com.TreeNut.ChatBot_Backend.service

import com.TreeNut.ChatBot_Backend.model.Chatroom
import com.TreeNut.ChatBot_Backend.model.Officeroom
import com.TreeNut.ChatBot_Backend.repository.ChatroomRepository
import com.TreeNut.ChatBot_Backend.repository.OfficeroomRepository
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono

@Service
class ChatroomService(
    private val chatroomRepository: ChatroomRepository,
    private val officeroomRepository: OfficeroomRepository, // OfficeroomRepository 추가
    private val webClient: WebClient.Builder
) {

    fun createOfficeroom(userid: String): Mono<Map<*, *>> {
        val requestBody = mapOf(
            "user_id" to userid
        )

        return webClient.build()
            .post()
            .uri("/mongo/office/create")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(requestBody)
            .retrieve()
            .bodyToMono(Map::class.java)
    }

    fun saveOfficeroom(userid: String, documentId: String): Officeroom {
        val newOfficeroom = Officeroom(
            userid = userid,
            mongoChatlog = documentId
        )
        return officeroomRepository.save(newOfficeroom) // OfficeroomRepository를 사용하여 저장
    }

    fun saveChatroom(userid: String, charactersIdx: Int = 0, documentId: String): Chatroom {
        val newChatroom = Chatroom(
            userid = userid,
            charactersIdx = charactersIdx,
            mongoChatlog = documentId
        )
        return chatroomRepository.save(newChatroom) // ChatroomRepository를 사용하여 저장
    }
}