Skip to content

Commit

Permalink
Merge pull request #81 from SUIN-BUNDANG-LINE/develop
Browse files Browse the repository at this point in the history
설문이용 v1.0.0 백엔드 배포
  • Loading branch information
JeongHunHui authored Oct 2, 2024
2 parents f10029b + 14d3c3f commit f88ba54
Show file tree
Hide file tree
Showing 143 changed files with 3,560 additions and 542 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/dev_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
# Spring Boot 관련
FRONTEND_BASE_URL: http://localhost:3000
BACKEND_BASE_URL: https://dev-api.sulmoon.io
AI_SERVER_BASE_URL: http://ai.sulmoon.io:8000
# New Relic 관련
NEW_RELIC_APP_NAME: sulmun2yong-development

Expand All @@ -99,5 +100,5 @@ jobs:
"docker pull ${{ env.DOCKER_ID }}/${{ env.DOCKER_IMAGE_NAME }}",
"docker stop ${{ env.CONTAINER_NAME }} || true",
"docker rm ${{ env.CONTAINER_NAME }} || true",
"docker run -d --name ${{ env.CONTAINER_NAME }} -p 8080:8080 -e SPRING_DATA_MONGODB_URI=${{ env.MONGODB_URL }} -e SPRING_DATA_MONGODB_DATABASE=${{ env.MONGODB_DATABASE }} -e FRONTEND_BASE-URL=${{ env.FRONTEND_BASE_URL }} -e BACKEND_BASE-URL=${{ env.BACKEND_BASE_URL }} -e NEW_RELIC_APP_NAME=${{ env.NEW_RELIC_APP_NAME }} ${{ env.DOCKER_ID }}/${{ env.DOCKER_IMAGE_NAME }}",
"docker run -d --name ${{ env.CONTAINER_NAME }} -p 8080:8080 -e SPRING_DATA_MONGODB_URI=${{ env.MONGODB_URL }} -e SPRING_DATA_MONGODB_DATABASE=${{ env.MONGODB_DATABASE }} -e FRONTEND_BASE-URL=${{ env.FRONTEND_BASE_URL }} -e BACKEND_BASE-URL=${{ env.BACKEND_BASE_URL }} -e AI-SERVER_BASE-URL=${{ env.AI_SERVER_BASE_URL }} -e NEW_RELIC_APP_NAME=${{ env.NEW_RELIC_APP_NAME }} ${{ env.DOCKER_ID }}/${{ env.DOCKER_IMAGE_NAME }}",
"docker image prune -af"]}'
16 changes: 16 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

Expand Down Expand Up @@ -50,13 +51,21 @@ dependencies {
// mongoDB
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")

// mongock
implementation("io.mongock:mongock-springboot:5.4.4")
implementation("io.mongock:mongodb-springdata-v4-driver:5.4.4")

// validation
implementation("org.springframework.boot:spring-boot-starter-validation")

// Swagger
implementation("org.springdoc:springdoc-openapi:2.3.0")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")

// AWS
implementation("software.amazon.awssdk:bom:2.27.24")
implementation("software.amazon.awssdk:s3:2.27.24")

// test
testImplementation("org.mockito:mockito-core:4.0.0")
testImplementation("org.mockito:mockito-inline:4.0.0")
Expand All @@ -75,6 +84,13 @@ kotlin {
}
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = JavaVersion.VERSION_17.toString()
}
}

tasks.withType<Test> {
useJUnitPlatform()
}
Expand Down
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ services:
volumes:
- sulmun2yong-test-mongodb:/data/db

sulmun2yong-test-redis:
image: redis:latest
container_name: sulmun2yong-test-redis
restart: always
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
expose:
- "6379"
ports:
- "6379:6379"
volumes:
- sulmun2yong-test-redis:/data

volumes:
sulmun2yong-test-db:
sulmun2yong-test-mongodb:
sulmun2yong-test-redis:
78 changes: 78 additions & 0 deletions src/main/kotlin/com/sbl/sulmun2yong/ai/adapter/GenerateAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.sbl.sulmun2yong.ai.adapter

import com.sbl.sulmun2yong.ai.dto.ChatSessionIdWithSurveyGeneratedByAI
import com.sbl.sulmun2yong.ai.dto.response.AISurveyGenerationResponse
import com.sbl.sulmun2yong.ai.exception.SurveyGenerationByAIFailedException
import com.sbl.sulmun2yong.global.error.PythonServerExceptionMapper
import com.sbl.sulmun2yong.survey.dto.response.SurveyMakeInfoResponse
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestTemplate

@Component
class GenerateAdapter(
@Value("\${ai-server.base-url}")
private val aiServerBaseUrl: String,
private val restTemplate: RestTemplate,
) {
fun requestSurveyGenerationWithFileUrl(
job: String,
groupName: String,
fileUrl: String,
userPrompt: String,
): AISurveyGenerationResponse {
val requestUrl = "$aiServerBaseUrl/generate/survey/file-url"

val requestBody =
mapOf(
"job" to job,
"group_name" to groupName,
"file_url" to fileUrl,
"user_prompt" to userPrompt,
)

return requestToGenerateSurvey(requestUrl, requestBody)
}

fun requestSurveyGenerationWithTextDocument(
job: String,
groupName: String,
textDocument: String,
userPrompt: String,
): AISurveyGenerationResponse {
val requestUrl = "$aiServerBaseUrl/generate/survey/text-document"

val requestBody =
mapOf(
"job" to job,
"group_name" to groupName,
"text_document" to textDocument,
"user_prompt" to userPrompt,
)

return requestToGenerateSurvey(requestUrl, requestBody)
}

private fun requestToGenerateSurvey(
requestUrl: String,
requestBody: Map<String, String>,
): AISurveyGenerationResponse {
val chatSessionIdWithSurveyGeneratedByAI =
try {
restTemplate
.postForEntity(
requestUrl,
requestBody,
ChatSessionIdWithSurveyGeneratedByAI::class.java,
).body ?: throw SurveyGenerationByAIFailedException()
} catch (e: HttpClientErrorException) {
throw PythonServerExceptionMapper.mapException(e)
}

val chatSessionId = chatSessionIdWithSurveyGeneratedByAI.chatSessionId
val survey = chatSessionIdWithSurveyGeneratedByAI.surveyGeneratedByAI.toDomain()
val surveyMakeInfoResponse = SurveyMakeInfoResponse.of(survey)
return AISurveyGenerationResponse.from(chatSessionId, surveyMakeInfoResponse)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.sbl.sulmun2yong.ai.controller

import com.sbl.sulmun2yong.ai.controller.doc.AIGenerateApiDoc
import com.sbl.sulmun2yong.ai.dto.request.SurveyGenerationWithFileUrlRequest
import com.sbl.sulmun2yong.ai.dto.request.SurveyGenerationWithTextDocumentRequest
import com.sbl.sulmun2yong.ai.service.GenerateService
import com.sbl.sulmun2yong.survey.dto.response.SurveyMakeInfoResponse
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.util.UUID

@RestController
@RequestMapping("/api/v1/ai/generate")
class AIGenerateController(
private val generateService: GenerateService,
) : AIGenerateApiDoc {
@PostMapping("/survey/file-url")
override fun generateSurveyWithFileUrl(
@RequestBody surveyGenerationWithFileUrlRequest: SurveyGenerationWithFileUrlRequest,
response: HttpServletResponse,
): ResponseEntity<SurveyMakeInfoResponse> {
val aiSurveyGenerationResponse = generateService.generateSurveyWithFileUrl(surveyGenerationWithFileUrlRequest)
setChatSessionIdCookie(response, aiSurveyGenerationResponse.chatSessionId)
return ResponseEntity.ok(aiSurveyGenerationResponse.generatedSurvey)
}

@PostMapping("/survey/text-document")
override fun generateSurveyWithTextDocument(
@RequestBody surveyGenerationWithTextDocumentRequest: SurveyGenerationWithTextDocumentRequest,
response: HttpServletResponse,
): ResponseEntity<SurveyMakeInfoResponse> {
val aiSurveyGenerationResponse = generateService.generateSurveyWithTextDocument(surveyGenerationWithTextDocumentRequest)
setChatSessionIdCookie(response, aiSurveyGenerationResponse.chatSessionId)
return ResponseEntity.ok(aiSurveyGenerationResponse.generatedSurvey)
}

private fun setChatSessionIdCookie(
response: HttpServletResponse,
chatSessionId: UUID,
) {
val cookie =
Cookie("chat-session-id", chatSessionId.toString()).apply {
maxAge = 3600
path = "/"
isHttpOnly = true
}

response.addCookie(cookie)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.sbl.sulmun2yong.ai.controller.doc

import com.sbl.sulmun2yong.ai.dto.request.SurveyGenerationWithFileUrlRequest
import com.sbl.sulmun2yong.ai.dto.request.SurveyGenerationWithTextDocumentRequest
import com.sbl.sulmun2yong.survey.dto.response.SurveyMakeInfoResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

@Tag(name = "AI", description = "AI 기능 관련 API")
interface AIGenerateApiDoc {
@Operation(summary = "파일을 통한 AI 설문 생성")
@PostMapping("/survey/file-url")
fun generateSurveyWithFileUrl(
@RequestBody surveyGenerationWithFileUrlRequest: SurveyGenerationWithFileUrlRequest,
response: HttpServletResponse,
): ResponseEntity<SurveyMakeInfoResponse>

@Operation(summary = "텍스트 입력을 통한 AI 설문 생성")
@PostMapping("/survey/text-document")
fun generateSurveyWithTextDocument(
@RequestBody surveyGenerationWithTextDocumentRequest: SurveyGenerationWithTextDocumentRequest,
response: HttpServletResponse,
): ResponseEntity<SurveyMakeInfoResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sbl.sulmun2yong.ai.dto

import java.util.UUID

class ChatSessionIdWithSurveyGeneratedByAI(
val chatSessionId: UUID,
val surveyGeneratedByAI: SurveyGeneratedByAI,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.sbl.sulmun2yong.ai.dto

import com.sbl.sulmun2yong.survey.domain.question.QuestionType
import com.sbl.sulmun2yong.survey.domain.question.choice.Choice
import com.sbl.sulmun2yong.survey.domain.question.choice.Choices
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardMultipleChoiceQuestion
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardSingleChoiceQuestion
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardTextQuestion
import java.util.UUID

class QuestionGeneratedByAI(
private val questionType: QuestionType,
private val title: String,
private val isRequired: Boolean,
private val choices: List<String>?,
private val isAllowOther: Boolean,
) {
fun toDomain() =
when (questionType) {
QuestionType.SINGLE_CHOICE ->
StandardSingleChoiceQuestion(
id = UUID.randomUUID(),
title = this.title,
description = DEFAULT_DESCRIPTION,
isRequired = this.isRequired,
// TODO: Document를 Domain클래스로 변환 중에 생긴 에러는 여기서 직접 반환하도록 수정
choices = Choices(this.choices?.map { Choice.Standard(it) } ?: listOf(), isAllowOther),
)
QuestionType.MULTIPLE_CHOICE ->
StandardMultipleChoiceQuestion(
id = UUID.randomUUID(),
title = this.title,
description = DEFAULT_DESCRIPTION,
isRequired = this.isRequired,
// TODO: Document를 Domain클래스로 변환 중에 생긴 에러는 여기서 직접 반환하도록 수정
choices = Choices(this.choices?.map { Choice.Standard(it) } ?: listOf(), isAllowOther),
)
QuestionType.TEXT_RESPONSE ->
StandardTextQuestion(
id = UUID.randomUUID(),
title = this.title,
description = DEFAULT_DESCRIPTION,
isRequired = this.isRequired,
)
}

companion object {
private const val DEFAULT_DESCRIPTION = ""
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/com/sbl/sulmun2yong/ai/dto/SectionGeneratedByAI.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.sbl.sulmun2yong.ai.dto

import com.sbl.sulmun2yong.survey.domain.routing.RoutingStrategy
import com.sbl.sulmun2yong.survey.domain.section.Section
import com.sbl.sulmun2yong.survey.domain.section.SectionId
import com.sbl.sulmun2yong.survey.domain.section.SectionIds

class SectionGeneratedByAI(
val title: String,
val description: String,
val questions: List<QuestionGeneratedByAI>,
) {
private val defaultRoutingStrategy = RoutingStrategy.NumericalOrder

fun toDomain(
sectionId: SectionId.Standard,
sectionIds: SectionIds,
): Section =
Section(
id = sectionId,
title = title,
description = description,
routingStrategy = defaultRoutingStrategy,
questions = questions.map { it.toDomain() },
sectionIds = sectionIds,
)
}
37 changes: 37 additions & 0 deletions src/main/kotlin/com/sbl/sulmun2yong/ai/dto/SurveyGeneratedByAI.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.sbl.sulmun2yong.ai.dto

import com.sbl.sulmun2yong.survey.domain.Survey
import com.sbl.sulmun2yong.survey.domain.section.SectionId
import com.sbl.sulmun2yong.survey.domain.section.SectionIds
import java.util.UUID

class SurveyGeneratedByAI(
private val title: String,
private val description: String,
private val finishMessage: String,
private val sections: List<SectionGeneratedByAI>,
) {
fun toDomain(): Survey {
val sectionIds = List(sections.size) { SectionId.Standard(UUID.randomUUID()) }
val sectionIdsManger = SectionIds.from(sectionIds)

val sections =
sections.mapIndexed { index, sectionGeneratedByAI ->
sectionGeneratedByAI.toDomain(
sectionIds[index],
sectionIdsManger,
)
}

val survey = Survey.create(UUID.randomUUID())
return survey.updateContent(
title = title,
description = description,
thumbnail = survey.thumbnail,
finishMessage = finishMessage,
rewardSetting = survey.rewardSetting,
isVisible = false,
sections = sections,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sbl.sulmun2yong.ai.dto.request

data class SurveyGenerationWithFileUrlRequest(
val job: String,
val groupName: String,
val fileUrl: String,
val userPrompt: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sbl.sulmun2yong.ai.dto.request

data class SurveyGenerationWithTextDocumentRequest(
val job: String,
val groupName: String,
val textDocument: String,
val userPrompt: String,
)
Loading

0 comments on commit f88ba54

Please sign in to comment.