Skip to content
Merged

Dev #88

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/src/docs/asciidoc/Report-API.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[[Report-API]]
= Report API

== 1. 피드 신고하기 04/19 수정

=== 요청(Request)
include::{snippets}/report-controller-test/report-feed/http-request.adoc[]

=== 요청 헤더(Request Headers)
include::{snippets}/report-controller-test/report-feed/request-headers.adoc[]

=== 요청 필드 설명(Request Fields)
include::{snippets}/report-controller-test/report-feed/request-fields.adoc[]

=== 응답(Response)
include::{snippets}/report-controller-test/report-feed/http-response.adoc[]

=== 응답 필드 설명(Response Fields)
include::{snippets}/report-controller-test/report-feed/response-fields.adoc[]
37 changes: 37 additions & 0 deletions api/src/main/kotlin/org/chewing/v1/controller/ai/AiController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.chewing.v1.controller.ai

import org.chewing.v1.dto.request.chat.ChatRequest
import org.chewing.v1.dto.response.chat.AiChatMessageResponse
import org.chewing.v1.facade.AiFacade
import org.chewing.v1.model.chat.room.ChatRoomId
import org.chewing.v1.model.user.UserId
import org.chewing.v1.service.chat.AiChatRoomService
import org.chewing.v1.util.aliases.SuccessResponseEntity
import org.chewing.v1.util.helper.ResponseHelper
import org.chewing.v1.util.security.CurrentUser
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

@Controller
class AiController(
private val aiFacade: AiFacade,
private val aiChatRoomService: AiChatRoomService,
) {
@PostMapping("/ai/chat/room")
fun createAiChatRoom(
@CurrentUser userId: UserId,
): SuccessResponseEntity<ChatRoomId> {
val chatRoomId = aiFacade.produceAiChatRoom(userId)
return ResponseHelper.success(chatRoomId)
}

@PostMapping("/ai/chat/room/prompt")
fun promptAiChatRoom(
@CurrentUser userId: UserId,
@RequestBody request: ChatRequest.Common,
): SuccessResponseEntity<AiChatMessageResponse> {
val prompt = aiFacade.processAiMessage(userId, request.toChatRoomId(), request.toMessage())
return ResponseHelper.success(AiChatMessageResponse.of(prompt))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.chewing.v1.controller.report

import org.chewing.v1.dto.request.report.ReportRequest
import org.chewing.v1.model.user.UserId
import org.chewing.v1.response.SuccessOnlyResponse
import org.chewing.v1.service.report.ReportService
import org.chewing.v1.util.aliases.SuccessResponseEntity
import org.chewing.v1.util.helper.ResponseHelper
import org.chewing.v1.util.security.CurrentUser
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

@RestController
@RequestMapping("/api/report")
class ReportController(
private val reportService: ReportService,
) {
@PostMapping("/feed")
fun reportFeed(
@RequestBody request: ReportRequest.Feed,
@CurrentUser userId: UserId,
): SuccessResponseEntity<SuccessOnlyResponse> {
reportService.reportFeed(userId, request.toFeedId(), request.toReport())
return ResponseHelper.successOnly()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.chewing.v1.dto.request.report

import org.chewing.v1.model.feed.FeedId
import org.chewing.v1.model.report.Report
import org.chewing.v1.model.report.ReportTargetType

class ReportRequest {
data class Feed(
val feedId: String,
val reason: String,
) {
fun toReport(): Report {
return Report.of(ReportTargetType.FEED, reason)
}
fun toFeedId() = FeedId.of(feedId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.chewing.v1.dto.response.chat

import org.chewing.v1.model.chat.message.ChatAiMessage

data class AiChatMessageResponse(
val messageId: String,
val type: String,
val senderId: String,
val timestamp: String,
val seqNumber: Int,
val text: String,
val senderType: String,
) {
companion object {
fun of(promptMessage: ChatAiMessage): AiChatMessageResponse {
return AiChatMessageResponse(
messageId = promptMessage.messageId,
type = promptMessage.type.name.lowercase(),
senderId = promptMessage.senderId.id,
timestamp = promptMessage.timestamp.toString(),
seqNumber = promptMessage.roomSequence.sequence,
text = promptMessage.text,
senderType = promptMessage.senderType.name.lowercase(),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ sealed class ChatLogResponse {
val comment: String,
) : ChatLogResponse()

data class Ai(
val messageId: String,
val type: String,
val senderId: String,
val timestamp: String,
val seqNumber: Int,
val text: String,
val senderType: String,
) : ChatLogResponse()

companion object {
fun from(chatLog: ChatLog): ChatLogResponse {
val formattedTime = chatLog.timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
Expand Down Expand Up @@ -133,6 +143,16 @@ sealed class ChatLogResponse {
content = chatLog.content,
comment = chatLog.comment,
)

is ChatAiLog -> Ai(
messageId = chatLog.messageId,
type = chatLog.type.name.lowercase(),
senderId = chatLog.senderId.id,
timestamp = formattedTime,
seqNumber = chatLog.roomSequence.sequence,
text = chatLog.text,
senderType = chatLog.senderType.name.lowercase(),
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.chewing.v1.dto.response.chat

import org.chewing.v1.error.ConflictException
import org.chewing.v1.error.ErrorCode
import org.chewing.v1.model.chat.log.ChatAiLog
import org.chewing.v1.model.chat.log.ChatCommentLog
import org.chewing.v1.model.chat.log.ChatFileLog
import org.chewing.v1.model.chat.log.ChatInviteLog
Expand Down Expand Up @@ -94,6 +97,8 @@ data class ThumbnailGroupChatRoomResponse(
chatRoomSequenceNumber = chatRoom.roomSequence.sequence,
chatRoomName = chatRoom.roomInfo.name,
)

is ChatAiLog -> throw ConflictException(ErrorCode.AI_NOT_SUPPORTED)
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ spring:
- fcm.yml
- ncp.yml
- media.yml
- openai.yml
- ai.yml
- optional:file:../.env[.properties]
- optional:file:./.env[.properties]
mvc:
Expand Down
2 changes: 2 additions & 0 deletions api/src/test/kotlin/org/chewing/v1/TestDataFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import org.chewing.v1.model.user.AccessStatus
import org.chewing.v1.model.user.User
import org.chewing.v1.model.user.UserInfo
import org.chewing.v1.model.user.UserId
import org.chewing.v1.model.user.UserRole
import org.springframework.mock.web.MockMultipartFile
import java.awt.Color
import java.awt.image.BufferedImage
Expand Down Expand Up @@ -220,6 +221,7 @@ object TestDataFactory {
"testPassword",
"testStatusMessage",
LocalDate.now(),
UserRole.USER,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.chewing.v1.facade.DirectChatFacade
import org.chewing.v1.facade.FriendFacade
import org.chewing.v1.facade.FriendFeedFacade
import org.chewing.v1.facade.GroupChatFacade
import org.chewing.v1.model.chat.log.ChatAiLog
import org.chewing.v1.model.chat.log.ChatCommentLog
import org.chewing.v1.model.chat.log.ChatFileLog
import org.chewing.v1.model.chat.log.ChatInviteLog
Expand Down Expand Up @@ -239,6 +240,8 @@ class MainControllerTest : RestDocsTest() {
body("$mediaPath.index", equalTo(media.index))
}
}

is ChatAiLog -> null
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.chewing.v1.controller

import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.chewing.v1.RestDocsTest
import org.chewing.v1.RestDocsUtils.requestAccessTokenFields
import org.chewing.v1.RestDocsUtils.requestPreprocessor
import org.chewing.v1.RestDocsUtils.responsePreprocessor
import org.chewing.v1.RestDocsUtils.responseSuccessFields
import org.chewing.v1.controller.report.ReportController
import org.chewing.v1.dto.request.report.ReportRequest
import org.chewing.v1.model.user.UserId
import org.chewing.v1.service.report.ReportService
import org.chewing.v1.util.converter.StringToFeedTypeConverter
import org.chewing.v1.util.handler.GlobalExceptionHandler
import org.chewing.v1.util.security.UserArgumentResolver
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import org.springframework.restdocs.payload.PayloadDocumentation.requestFields
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.test.context.ActiveProfiles

@ActiveProfiles("test")
class ReportControllerTest : RestDocsTest() {

private lateinit var reportService: ReportService
private lateinit var reportController: ReportController
private lateinit var exceptionHandler: GlobalExceptionHandler
private lateinit var userArgumentResolver: UserArgumentResolver
private lateinit var feedTypeConverter: StringToFeedTypeConverter

@BeforeEach
fun setUp() {
reportService = mockk()
exceptionHandler = GlobalExceptionHandler()
userArgumentResolver = UserArgumentResolver()
feedTypeConverter = StringToFeedTypeConverter()
reportController = ReportController(reportService)
mockMvc = mockController(reportController, exceptionHandler, userArgumentResolver)
val userId = UserId.of("testUserId")
val authentication = UsernamePasswordAuthenticationToken(userId, null)
SecurityContextHolder.getContext().authentication = authentication
}

@Test
@DisplayName("피드 신고 API 테스트")
fun reportFeed() {
val requestBody = ReportRequest.Feed(
feedId = "testFeedId",
reason = "신고 사유",
)

every { reportService.reportFeed(any(), any(), any()) } just Runs

given()
.setupAuthenticatedJsonRequest()
.body(requestBody)
.post("/api/report/feed")
.then()
.assertCommonSuccessResponse()
.apply(
document(
"{class-name}/{method-name}",
requestPreprocessor(),
responsePreprocessor(),
requestFields(
fieldWithPath("feedId").description("신고할 피드 ID"),
fieldWithPath("reason").description("신고 사유"),
),
requestAccessTokenFields(),
responseSuccessFields(),
),
)
}
}
4 changes: 4 additions & 0 deletions common/src/main/kotlin/org/chewing/v1/error/ErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ enum class ErrorCode(

INVALID_TYPE("INVALID_1", "잘못된 타입입니다."),

AI_PROMPT_FAILED("AI_1", "AI 프롬프트를 실패하였습니다."),
AI_NOTIFICATION_NOT_SUPPORTED("AI_2", "AI 알림을 지원하지 않습니다."),
AI_WEBSOCKET_NOT_SUPPORTED("AI_3", "AI 웹소켓을 지원하지 않습니다."),
AI_NOT_SUPPORTED("AI_4", "AI를 지원하지 않습니다."),
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.chewing.v1.external

import org.chewing.v1.model.ai.Prompt

interface ExternalAiClient {
suspend fun prompt(prompts: List<Prompt>): String?
}
41 changes: 41 additions & 0 deletions domain/src/main/kotlin/org/chewing/v1/facade/AiFacade.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.chewing.v1.facade

import org.chewing.v1.implementation.ai.AiUserGenerator
import org.chewing.v1.model.chat.member.SenderType
import org.chewing.v1.model.chat.message.ChatAiMessage
import org.chewing.v1.model.chat.room.ChatRoomId
import org.chewing.v1.model.chat.room.ChatRoomType
import org.chewing.v1.model.user.UserId
import org.chewing.v1.service.ai.AiPromptService
import org.chewing.v1.service.chat.AiChatRoomService
import org.chewing.v1.service.chat.ChatLogService
import org.springframework.stereotype.Service

@Service
class AiFacade(
private val aiChatRoomService: AiChatRoomService,
private val aiPromptService: AiPromptService,
private val chatLogService: ChatLogService,
private val aiUserGenerator: AiUserGenerator,
) {
fun processAiMessage(
userId: UserId,
chatRoomId: ChatRoomId,
text: String,
): ChatAiMessage {
val aiChatRoom = aiChatRoomService.getAiChatRoom(chatRoomId, userId)
val chatSequence = aiChatRoomService.increaseDirectChatRoomSequence(aiChatRoom.chatRoomId)
chatLogService.aiMessage(aiChatRoom.chatRoomId, userId, chatSequence, text, ChatRoomType.AI, SenderType.USER)
val chatLogs = chatLogService.getChatLogs(aiChatRoom.chatRoomId, chatSequence.sequence, 0)
val aiPromptMessage = aiPromptService.prompt(chatLogs)
val aiUserId = aiUserGenerator.getAiUserId()
val aiChatMessage = chatLogService.aiMessage(aiChatRoom.chatRoomId, aiUserId, chatSequence, aiPromptMessage, ChatRoomType.AI, SenderType.AI)
return aiChatMessage
}

fun produceAiChatRoom(
userId: UserId,
): ChatRoomId {
return aiChatRoomService.createAiChatRoom(userId)
}
}
Loading