Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
import team.darkmoderap.aikon.domain.avatar.dto.AvatarChangeResDto
import team.darkmoderap.aikon.domain.avatar.dto.CreateAvatarReqDto
import team.darkmoderap.aikon.domain.avatar.dto.CreateAvatarResDto
import team.darkmoderap.aikon.domain.avatar.dto.CreateFeedbackReqDto
import team.darkmoderap.aikon.domain.avatar.dto.GetAvatarResDto
import team.darkmoderap.aikon.domain.avatar.dto.UpdateAvatarReqDto
import team.darkmoderap.aikon.domain.avatar.dto.UpdateDefaultStyleReqDto
import team.darkmoderap.aikon.domain.avatar.service.CreateAvatarService
import team.darkmoderap.aikon.domain.avatar.service.CreateFeedbackService
import team.darkmoderap.aikon.domain.avatar.service.DeleteAllAvatarsService
import team.darkmoderap.aikon.domain.avatar.service.DeleteAvatarService
import team.darkmoderap.aikon.domain.avatar.service.GetAvatarByPassService
Expand All @@ -59,6 +61,7 @@ class AvatarController(
private val updateDefaultStyleService: UpdateDefaultStyleService,
private val deleteAvatarService: DeleteAvatarService,
private val deleteAllAvatarsService: DeleteAllAvatarsService,
private val createFeedbackService: CreateFeedbackService,
private val validator: Validator,
private val objectMapper: ObjectMapper,
) {
Expand Down Expand Up @@ -209,6 +212,29 @@ class AvatarController(
deleteAllAvatarsService.execute()
}

@Operation(summary = "아바타 피드백 등록")
@ApiResponses(
ApiResponse(responseCode = "201", description = "피드백 등록 성공"),
ApiResponse(
responseCode = "400",
description = "잘못된 입력값",
content = [Content(schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "404",
description = "아바타를 찾을 수 없음",
content = [Content(schema = Schema(implementation = ErrorResponse::class))],
),
)
@PostMapping("/{avatarId}/feedback")
@ResponseStatus(HttpStatus.CREATED)
fun createFeedback(
@PathVariable avatarId: Long,
@Valid @RequestBody reqDto: CreateFeedbackReqDto,
) {
createFeedbackService.execute(avatarId, reqDto)
}

private fun parseCreateAvatarReqDto(rawReqDto: String): CreateAvatarReqDto {
val reqDto =
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package team.darkmoderap.aikon.domain.avatar.controller

import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import team.darkmoderap.aikon.domain.avatar.dto.AvatarGenerationCallbackReqDto
import team.darkmoderap.aikon.domain.avatar.service.HandleAvatarGenerationCallbackService
import team.darkmoderap.aikon.global.common.error.AikonException
import team.darkmoderap.aikon.global.common.error.ErrorCode

@RestController
@RequestMapping("/internal/ai/avatar-generations")
class InternalAiAvatarGenerationController(
private val handleAvatarGenerationCallbackService: HandleAvatarGenerationCallbackService,
@Value("\${internal.callback-secret}") private val callbackSecret: String,
) {
@PostMapping("/callback")
@ResponseStatus(HttpStatus.OK)
fun callback(
@RequestHeader("X-Internal-Secret") secret: String,
@RequestBody reqDto: AvatarGenerationCallbackReqDto,
) {
if (secret != callbackSecret) {
throw AikonException(ErrorCode.INVALID_INTERNAL_SECRET)
}
Comment thread
cfcromn marked this conversation as resolved.
Outdated
handleAvatarGenerationCallbackService.execute(reqDto)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package team.darkmoderap.aikon.domain.avatar.dto

data class AvatarGenerationCallbackReqDto(
val avatarId: Long,
val jobId: String,
val status: String,
val generatedImageUri: String?,
val modelName: String?,
val promptVersion: String?,
val promptText: String?,
val durationMs: Long?,
val errorCode: String?,
val errorMessage: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package team.darkmoderap.aikon.domain.avatar.dto

import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.NotNull
import team.darkmoderap.aikon.domain.avatar.entity.enum.FeedbackRating
import team.darkmoderap.aikon.domain.avatar.entity.enum.FeedbackReason

data class CreateFeedbackReqDto(
@field:NotNull
val rating: FeedbackRating?,
@field:NotEmpty
val reasons: List<FeedbackReason>?,
val comment: String?,
@field:NotNull
val trainingConsent: Boolean?,
@field:NotNull
val feedbackUseConsent: Boolean?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package team.darkmoderap.aikon.domain.avatar.dto

data class FastApiFeedbackReqDto(
val avatarId: Long,
val jobId: String?,
val rating: String,
val reasons: List<String>,
val comment: String?,
val trainingConsent: Boolean,
val feedbackUseConsent: Boolean,
val style: String,
val gender: String,
val ageRange: String,
val promptVersion: String?,
val modelName: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.darkmoderap.aikon.domain.avatar.dto

data class FastApiGenerationReqDto(
val avatarId: Long,
val sourceImageUri: String,
val style: String,
val gender: String,
val ageRange: String,
val callbackUrl: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package team.darkmoderap.aikon.domain.avatar.dto

data class FastApiGenerationResDto(
val jobId: String,
val status: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import team.darkmoderap.aikon.domain.avatar.entity.enum.Gender
import team.darkmoderap.aikon.domain.avatar.entity.enum.GenerationStatus
import team.darkmoderap.aikon.domain.avatar.entity.enum.Style
import team.darkmoderap.aikon.global.common.entity.BaseEntity
import java.time.Instant

@Entity
@Table(name = "avatars")
Expand All @@ -35,6 +36,22 @@ class AvatarEntity(
var imageUrl: String? = null,
@Column(name = "pass_url", unique = true)
var passUrl: String? = null,
@Column(name = "ai_job_id", length = 36)
var aiJobId: String? = null,
@Column(name = "source_image_uri", length = 500)
var sourceImageUri: String? = null,
@Column(name = "model_name", length = 100)
var modelName: String? = null,
@Column(name = "prompt_version", length = 20)
var promptVersion: String? = null,
@Column(name = "prompt_text", columnDefinition = "TEXT")
var promptText: String? = null,
@Column(name = "duration_ms")
var durationMs: Long? = null,
@Column(name = "error_code", length = 50)
var errorCode: String? = null,
@Column(name = "completed_at")
var completedAt: Instant? = null,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
Expand All @@ -49,12 +66,29 @@ class AvatarEntity(
this.ageRange = ageRange
}

fun completeGeneration(imageUrl: String) {
fun completeGeneration(
imageUrl: String,
modelName: String?,
promptVersion: String?,
promptText: String?,
durationMs: Long?,
completedAt: Instant?,
) {
this.imageUrl = imageUrl
this.generationStatus = GenerationStatus.COMPLETED
this.modelName = modelName
this.promptVersion = promptVersion
this.promptText = promptText
this.durationMs = durationMs
this.completedAt = completedAt
}

fun failGeneration() {
fun failGeneration(
errorCode: String?,
completedAt: Instant?,
) {
this.generationStatus = GenerationStatus.FAILED
this.errorCode = errorCode
this.completedAt = completedAt
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package team.darkmoderap.aikon.domain.avatar.entity

import jakarta.persistence.CollectionTable
import jakarta.persistence.Column
import jakarta.persistence.ElementCollection
import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import team.darkmoderap.aikon.domain.avatar.entity.enum.FeedbackRating
import team.darkmoderap.aikon.domain.avatar.entity.enum.FeedbackReason
import team.darkmoderap.aikon.global.common.entity.BaseEntity

@Entity
@Table(
name = "avatar_feedbacks",
uniqueConstraints = [UniqueConstraint(columnNames = ["avatar_id"])],
)
class AvatarFeedback(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "avatar_id", nullable = false)
val avatar: AvatarEntity,
@Enumerated(EnumType.STRING)
@Column(nullable = false)
var rating: FeedbackRating,
@ElementCollection
@CollectionTable(name = "avatar_feedback_reasons", joinColumns = [JoinColumn(name = "feedback_id")])
@Enumerated(EnumType.STRING)
@Column(name = "reason", nullable = false)
var reasons: MutableList<FeedbackReason>,
@Column(length = 500)
var comment: String? = null,
@Column(name = "training_consent", nullable = false)
var trainingConsent: Boolean = false,
@Column(name = "feedback_use_consent", nullable = false)
var feedbackUseConsent: Boolean = false,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
) : BaseEntity() {
fun update(
rating: FeedbackRating,
reasons: List<FeedbackReason>,
comment: String?,
trainingConsent: Boolean,
feedbackUseConsent: Boolean,
) {
this.rating = rating
this.reasons.clear()
this.reasons.addAll(reasons)
this.comment = comment
this.trainingConsent = trainingConsent
this.feedbackUseConsent = feedbackUseConsent
}
Comment thread
cfcromn marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package team.darkmoderap.aikon.domain.avatar.entity.enum

enum class FeedbackRating {
LIKE,
DISLIKE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package team.darkmoderap.aikon.domain.avatar.entity.enum

enum class FeedbackReason {
FACE_SIMILARITY,
STYLE_MATCH,
BACKGROUND,
DETAIL_BROKEN,
AGE_MISMATCH,
TOO_DIFFERENT,
GOOD_QUALITY,
BAD_QUALITY,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package team.darkmoderap.aikon.domain.avatar.repository

import org.springframework.data.jpa.repository.JpaRepository
import team.darkmoderap.aikon.domain.avatar.entity.AvatarFeedback

interface AvatarFeedbackRepository : JpaRepository<AvatarFeedback, Long> {
fun findByAvatarId(avatarId: Long): AvatarFeedback?
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@ interface AvatarImageStorage {
image: GeneratedAvatarImage,
): String

fun uploadSourceImage(
avatarId: Long,
bytes: ByteArray,
mimeType: String,
): String

fun toPublicUrl(s3Uri: String): String

fun delete(imageUrl: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import team.darkmoderap.aikon.domain.avatar.dto.CreateAvatarReqDto
import team.darkmoderap.aikon.domain.avatar.dto.CreateAvatarResDto
import team.darkmoderap.aikon.domain.avatar.entity.AvatarEntity
import team.darkmoderap.aikon.domain.avatar.entity.enum.GenerationStatus
import team.darkmoderap.aikon.domain.avatar.entity.enum.Style
import team.darkmoderap.aikon.domain.avatar.event.AvatarCreatedEvent
import team.darkmoderap.aikon.domain.avatar.event.AvatarListChangedEvent
import team.darkmoderap.aikon.domain.avatar.repository.AvatarRepository
Expand All @@ -31,6 +32,10 @@ class CreateAvatarServiceImpl(
val gender = reqDto.gender ?: throw AikonException(ErrorCode.INVALID_INPUT_VALUE)
val style = reqDto.style ?: throw AikonException(ErrorCode.INVALID_INPUT_VALUE)
val ageRange = reqDto.ageRange ?: throw AikonException(ErrorCode.INVALID_INPUT_VALUE)

if (style == Style.ENHANCED) {
throw AikonException(ErrorCode.AVATAR_STYLE_NOT_SUPPORTED_BY_AI)
}
val avatar =
try {
avatarRepository.saveAndFlush(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package team.darkmoderap.aikon.domain.avatar.service

import team.darkmoderap.aikon.domain.avatar.dto.CreateFeedbackReqDto

interface CreateFeedbackService {
fun execute(
avatarId: Long,
reqDto: CreateFeedbackReqDto,
)
}
Loading
Loading