diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionClient.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionClient.kt deleted file mode 100644 index 7fc5242a..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionClient.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.grida.chat - -import org.grida.config.AiClientConfig -import org.springframework.cloud.openfeign.FeignClient -import org.springframework.web.bind.annotation.PostMapping - -@FeignClient( - name = "OpenAiChatCompletion", - url = "https://api.openai.com/v1/chat/completions", - configuration = [AiClientConfig::class], -) -interface ChatCompletionClient { - @PostMapping - fun chat(chatCompletionRequest: ChatCompletionRequest): ChatCompletionResponse -} diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionDto.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionDto.kt deleted file mode 100644 index baa676ee..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionDto.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.grida.chat - -data class ChatCompletionRequest( - val model: String? = DEFAULT_CHAT_COMPLETION_MODEL, - val messages: List, -) { - constructor( - model: String, - role: String, - prompt: String, - ) : this( - model, - listOf(ChatMessage(role, prompt)), - ) -} - -data class ChatCompletionResponse( - val choices: List, -) { - val result: String - get() = choices[0].message.content -} - -data class Choice( - val message: ChatMessage, -) - -data class ChatMessage( - val role: String? = DEFAULT_CHAT_COMPLETION_ROLE, - val content: String, -) diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionOptions.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionOptions.kt deleted file mode 100644 index c7f95b3d..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/chat/ChatCompletionOptions.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.grida.chat - -const val DEFAULT_CHAT_COMPLETION_MODEL = "gpt-4-1106-preview" -const val DEFAULT_CHAT_COMPLETION_ROLE = "system" diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt deleted file mode 100644 index 4c690f58..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt +++ /dev/null @@ -1,32 +0,0 @@ -package org.grida.config - -import feign.RequestInterceptor -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.context.properties.ConfigurationPropertiesScan -import org.springframework.cloud.openfeign.EnableFeignClients -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Configuration -@ConfigurationPropertiesScan -@EnableFeignClients(basePackages = ["org.grida"]) -class AiClientConfig( - @Value("\${openai.secret-key}") val secretKey: String -) { - - @Bean - fun requestInterceptor(): RequestInterceptor { - return RequestInterceptor { - it.apply { - header(CONTENT_TYPE_KEY, CONTENT_TYPE_VALUE) - header(SECRET_KEY_KEY, "Bearer $secretKey") - } - } - } - - companion object { - private const val CONTENT_TYPE_KEY = "Content-Type" - private const val CONTENT_TYPE_VALUE = "application/json" - private const val SECRET_KEY_KEY = "Authorization" - } -} diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateClient.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateClient.kt deleted file mode 100644 index e27c7623..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateClient.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.grida.image - -import org.grida.config.AiClientConfig -import org.springframework.cloud.openfeign.FeignClient -import org.springframework.web.bind.annotation.PostMapping - -@FeignClient( - name = "OpenAiImageGeneration", - url = "https://api.openai.com/v1/images/generations", - configuration = [AiClientConfig::class] -) -interface ImageGenerateClient { - - @PostMapping - fun generate(request: ImageGenerateRequest): ImageGenerateResponse -} diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateDtos.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateDtos.kt deleted file mode 100644 index 6cba6355..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateDtos.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.grida.image - -data class ImageGenerateRequest( - val prompt: String, - val model: String? = DEFAULT_IMAGE_GENERATE_MODEL, - val n: Int? = DEFAULT_IMAGE_GENERATE_N, - val size: String? = DEFAULT_IMAGE_GENERATE_SIZE, -) - -data class ImageGenerateResponse( - val data: List, -) { - val imageUrl: String - get() = data[0].url - - val imageUrls: List - get() = data.map { it.url } -} - -data class ImageUrl( - val url: String, -) diff --git a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateOptions.kt b/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateOptions.kt deleted file mode 100644 index e2135095..00000000 --- a/grida-clients/ai-client/src/main/kotlin/org/grida/image/ImageGenerateOptions.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.grida.image - -const val DEFAULT_IMAGE_GENERATE_MODEL = "dall-e-3" -const val DEFAULT_IMAGE_GENERATE_N = 1 -const val DEFAULT_IMAGE_GENERATE_SIZE = "1024x1024" diff --git a/grida-clients/kakao-client/build.gradle.kts b/grida-clients/kakao-client/build.gradle.kts new file mode 100644 index 00000000..aabc4cd5 --- /dev/null +++ b/grida-clients/kakao-client/build.gradle.kts @@ -0,0 +1,8 @@ +dependencies { + // feign client + implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.1.8") + implementation("io.github.openfeign:feign-jackson:12.1") + + // jackson + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthApi.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthApi.kt new file mode 100644 index 00000000..e0e45a51 --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthApi.kt @@ -0,0 +1,27 @@ +package org.grida.auth + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestParam + +@FeignClient( + name = "KakaoAuth", + url = "https://kauth.kakao.com/oauth", +) +interface KakaoAuthApi { + + @PostMapping("/token", headers = ["Content-type=application/x-www-form-urlencoded;charset=utf-8"]) + fun provideToken( + @RequestParam("grant_type") grantType: String, + @RequestParam("client_id") clientId: String, + @RequestParam("redirect_uri") redirectUri: String, + @RequestParam("code") code: String + ): KakaoAuthResponse + + @PostMapping("/token", headers = ["Content-type=application/x-www-form-urlencoded;charset=utf-8"]) + fun refreshToken( + @RequestParam("grant_type") grantType: String, + @RequestParam("client_id") clientId: String, + @RequestParam("refresh_token") refreshToken: String + ): KakaoAuthResponse +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthClient.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthClient.kt new file mode 100644 index 00000000..7c7c7a97 --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthClient.kt @@ -0,0 +1,39 @@ +package org.grida.auth + +import feign.FeignException +import org.grida.config.KakaoProperties +import org.springframework.stereotype.Component + +@Component +class KakaoAuthClient( + private val kakaoAuthApi: KakaoAuthApi, + private val kakaoProperties: KakaoProperties +) { + + fun provideAuthToken(code: String): KakaoAuthToken { + try { + val response = kakaoAuthApi.provideToken( + grantType = "authorization_code", + clientId = kakaoProperties.appKey, + redirectUri = kakaoProperties.redirectUri, + code = code + ) + return response.toKakaoAuthToken() + } catch (e: FeignException) { + throw IllegalArgumentException(e.stackTraceToString()) + } + } + + fun refreshToken(refreshToken: String): KakaoAuthToken { + try { + val response = kakaoAuthApi.refreshToken( + grantType = "refresh_token", + clientId = kakaoProperties.appKey, + refreshToken = refreshToken + ) + return response.toKakaoAuthToken() + } catch (e: FeignException) { + throw IllegalArgumentException(e.stackTraceToString()) + } + } +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthResponse.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthResponse.kt new file mode 100644 index 00000000..6285cccf --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthResponse.kt @@ -0,0 +1,21 @@ +package org.grida.auth + +import com.fasterxml.jackson.annotation.JsonProperty + +data class KakaoAuthResponse( + @JsonProperty("token_type") + val tokenType: String, + @JsonProperty("access_token") + val accessToken: String, + @JsonProperty("expires_in") + val expiresId: Int, + @JsonProperty("refresh_token") + val refreshToken: String?, + @JsonProperty("refresh_token_expires_in") + val refreshTokenExpiredIn: Int? +) { + + fun toKakaoAuthToken(): KakaoAuthToken { + return KakaoAuthToken(accessToken, refreshToken ?: "") + } +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthToken.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthToken.kt new file mode 100644 index 00000000..ba8596e9 --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/auth/KakaoAuthToken.kt @@ -0,0 +1,6 @@ +package org.grida.auth + +data class KakaoAuthToken( + val accessToken: String, + val refreshToken: String +) diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoClientConfig.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoClientConfig.kt new file mode 100644 index 00000000..f2a45e4c --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoClientConfig.kt @@ -0,0 +1,15 @@ +package org.grida.config + +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Configuration + +@Configuration +@ConfigurationPropertiesScan +@EnableFeignClients( + basePackages = [ + "org.grida.auth", + "org.grida.user" + ] +) +class KakaoClientConfig diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoProperties.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoProperties.kt new file mode 100644 index 00000000..651e6cca --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/config/KakaoProperties.kt @@ -0,0 +1,11 @@ +package org.grida.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding + +@ConstructorBinding +@ConfigurationProperties("kakao") +data class KakaoProperties( + val appKey: String, + val redirectUri: String +) diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserApi.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserApi.kt new file mode 100644 index 00000000..d49c10ba --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserApi.kt @@ -0,0 +1,20 @@ +package org.grida.user + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestParam + +@FeignClient( + name = "KakaoUser", + url = "https://kapi.kakao.com/v2/user", +) +interface KakaoUserApi { + + @GetMapping("/me", headers = ["Content-type=application/x-www-form-urlencoded;charset=utf-8"]) + fun readUserProfile( + @RequestHeader("Authorization") bearerToken: String, + @RequestParam("property_keys") propertyKeys: String, + @RequestParam("secure_resource") secureResource: Boolean + ): KakaoUserProfileResponse +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserClient.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserClient.kt new file mode 100644 index 00000000..eade0f6a --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserClient.kt @@ -0,0 +1,23 @@ +package org.grida.user + +import org.springframework.stereotype.Component + +@Component +class KakaoUserClient( + private val kakaoUserApi: KakaoUserApi +) { + + fun readUserProfile(accessToken: String): KakaoUserProfile { + val response = kakaoUserApi.readUserProfile( + bearerToken = "Bearer $accessToken", + propertyKeys = QUERY_PROPERTY_KEYS, + secureResource = true + ) + + return response.toKakaoUserProfile() + } + + companion object { + const val QUERY_PROPERTY_KEYS = "[\"kakao_account.profile\"]" + } +} diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfile.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfile.kt new file mode 100644 index 00000000..8085fa7d --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfile.kt @@ -0,0 +1,6 @@ +package org.grida.user + +data class KakaoUserProfile( + val id: String, + val name: String +) diff --git a/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfileResponse.kt b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfileResponse.kt new file mode 100644 index 00000000..ba0dceff --- /dev/null +++ b/grida-clients/kakao-client/src/main/kotlin/org/grida/user/KakaoUserProfileResponse.kt @@ -0,0 +1,24 @@ +package org.grida.user + +import com.fasterxml.jackson.annotation.JsonProperty + +data class KakaoUserProfileResponse( + val id: String, + @JsonProperty("kakao_account") + val properties: KakaoAccount +) { + fun toKakaoUserProfile(): KakaoUserProfile { + return KakaoUserProfile( + id = id, + name = properties.profile.nickname, + ) + } +} + +data class KakaoAccount( + val profile: KakaoAccountProfile +) + +data class KakaoAccountProfile( + val nickname: String, +) diff --git a/grida-clients/kakao-client/src/main/resources/application.yml b/grida-clients/kakao-client/src/main/resources/application.yml new file mode 100644 index 00000000..e69de29b diff --git a/grida-clients/ai-client/build.gradle.kts b/grida-clients/openai-client/build.gradle.kts similarity index 100% rename from grida-clients/ai-client/build.gradle.kts rename to grida-clients/openai-client/build.gradle.kts diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatApi.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatApi.kt new file mode 100644 index 00000000..2ae8a86e --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatApi.kt @@ -0,0 +1,19 @@ +package org.grida.chat + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader + +@FeignClient( + name = "OpenAiChatCompletion", + url = "https://api.openai.com/v1/chat/completions", +) +interface OpenAiChatApi { + + @PostMapping(headers = ["Content-Type=application/json"]) + fun chat( + @RequestHeader("Authorization") secretKey: String, + @RequestBody request: OpenAiChatRequest + ): OpenAiChatResponse +} diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatClient.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatClient.kt new file mode 100644 index 00000000..a04c9c50 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatClient.kt @@ -0,0 +1,30 @@ +package org.grida.chat + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Component + +@Component +class OpenAiChatClient( + private val openAiChatApi: OpenAiChatApi, + private val openAiSecretKey: String, + private val objectMapper: ObjectMapper +) { + + fun chat( + prompt: String, + valueType: Class + ): T { + val bearerToken = "Bearer $openAiSecretKey" + val request = OpenAiChatRequest( + model = "gpt-4o", + role = "system", + prompt = prompt, + responseFormat = "json_object" + ) + + val response = openAiChatApi.chat(bearerToken, request) + val result = response.result + + return objectMapper.readValue(result, valueType) + } +} diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatMessage.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatMessage.kt new file mode 100644 index 00000000..2ec4d040 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatMessage.kt @@ -0,0 +1,6 @@ +package org.grida.chat + +class OpenAiChatMessage( + val role: String, + val content: String, +) diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatRequest.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatRequest.kt new file mode 100644 index 00000000..8dfb02b9 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatRequest.kt @@ -0,0 +1,25 @@ +package org.grida.chat + +import com.fasterxml.jackson.annotation.JsonProperty + +data class OpenAiChatRequest( + val model: String, + val messages: List, + @JsonProperty("response_format") + val responseFormat: OpenAiResponseType +) { + constructor( + model: String, + role: String, + prompt: String, + responseFormat: String + ) : this( + model, + listOf(OpenAiChatMessage(role, prompt)), + OpenAiResponseType(responseFormat) + ) +} + +data class OpenAiResponseType( + val type: String +) diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatResponse.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatResponse.kt new file mode 100644 index 00000000..32b0fbb4 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/chat/OpenAiChatResponse.kt @@ -0,0 +1,12 @@ +package org.grida.chat + +data class OpenAiChatResponse( + val choices: List, +) { + val result: String + get() = choices[0].message.content +} + +data class OpenAiChoice( + val message: OpenAiChatMessage, +) diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt new file mode 100644 index 00000000..a2c33375 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/config/AiClientConfig.kt @@ -0,0 +1,23 @@ +package org.grida.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableFeignClients( + basePackages = [ + "org.grida.chat", + "org.grida.image" + ] +) +class AiClientConfig { + + @Bean + fun openAiSecretKey( + @Value("\${openai.secret-key}") secretKey: String + ): String { + return secretKey + } +} diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageApi.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageApi.kt new file mode 100644 index 00000000..76baa381 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageApi.kt @@ -0,0 +1,21 @@ +package org.grida.image + +import org.grida.config.AiClientConfig +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader + +@FeignClient( + name = "OpenAiImage", + url = "https://api.openai.com/v1/images/generations", + configuration = [AiClientConfig::class] +) +interface OpenAiImageApi { + + @PostMapping(headers = ["Content-Type=application/json"]) + fun generate( + @RequestHeader("Authorization") secretKey: String, + @RequestBody request: OpenAiImageRequest + ): OpenAiImageResponse +} diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageClient.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageClient.kt new file mode 100644 index 00000000..4f547274 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageClient.kt @@ -0,0 +1,28 @@ +package org.grida.image + +import org.springframework.stereotype.Component + +@Component +class OpenAiImageClient( + private val openaiImageApi: OpenAiImageApi, + private val openAiSecretKey: String, +) { + + fun generate(prompt: String): String { + val request = OpenAiImageRequest( + prompt = prompt, + model = DEFAULT_IMAGE_GENERATE_MODEL, + n = DEFAULT_IMAGE_GENERATE_N, + size = DEFAULT_IMAGE_GENERATE_SIZE + ) + + val response = openaiImageApi.generate(openAiSecretKey, request) + return response.imageUrl + } + + companion object { + private const val DEFAULT_IMAGE_GENERATE_MODEL = "dall-e-3" + private const val DEFAULT_IMAGE_GENERATE_N = 1 + private const val DEFAULT_IMAGE_GENERATE_SIZE = "1024x1024" + } +} diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageRequest.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageRequest.kt new file mode 100644 index 00000000..316e0c85 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageRequest.kt @@ -0,0 +1,8 @@ +package org.grida.image + +data class OpenAiImageRequest( + val prompt: String, + val model: String, + val n: Int, + val size: String, +) diff --git a/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageResponse.kt b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageResponse.kt new file mode 100644 index 00000000..e2a73b25 --- /dev/null +++ b/grida-clients/openai-client/src/main/kotlin/org/grida/image/OpenAiImageResponse.kt @@ -0,0 +1,15 @@ +package org.grida.image + +data class OpenAiImageResponse( + val data: List, +) { + val imageUrl: String + get() = data[0].url + + val imageUrls: List + get() = data.map { it.url } +} + +data class OpenAiImageUrl( + val url: String, +) diff --git a/grida-clients/ai-client/src/main/resources/application.yml b/grida-clients/openai-client/src/main/resources/application.yml similarity index 100% rename from grida-clients/ai-client/src/main/resources/application.yml rename to grida-clients/openai-client/src/main/resources/application.yml diff --git a/grida-clients/storage-client/src/main/kotlin/org/grida/StorageClient.kt b/grida-clients/storage-client/src/main/kotlin/org/grida/StorageClient.kt index d1c39445..e8921036 100644 --- a/grida-clients/storage-client/src/main/kotlin/org/grida/StorageClient.kt +++ b/grida-clients/storage-client/src/main/kotlin/org/grida/StorageClient.kt @@ -5,7 +5,6 @@ import com.amazonaws.services.s3.AmazonS3 import org.grida.config.StorageClientProperties import org.grida.error.FileUploadFail import org.grida.error.GridaException -import org.grida.error.StorageClientErrorType import org.springframework.stereotype.Component import java.io.File diff --git a/grida-common/src/main/kotlin/org/grida/error/ErrorType.kt b/grida-common/src/main/kotlin/org/grida/error/ErrorType.kt index e727cadc..d796fa3c 100644 --- a/grida-common/src/main/kotlin/org/grida/error/ErrorType.kt +++ b/grida-common/src/main/kotlin/org/grida/error/ErrorType.kt @@ -5,4 +5,4 @@ interface ErrorType { val errorCode: String val message: String val logLevel: LogLevel -} \ No newline at end of file +} diff --git a/grida-common/src/main/kotlin/org/grida/error/GridaException.kt b/grida-common/src/main/kotlin/org/grida/error/GridaException.kt index 837d339a..96e63685 100644 --- a/grida-common/src/main/kotlin/org/grida/error/GridaException.kt +++ b/grida-common/src/main/kotlin/org/grida/error/GridaException.kt @@ -2,6 +2,4 @@ package org.grida.error class GridaException( val errorType: ErrorType -) : RuntimeException(errorType.message) { - -} +) : RuntimeException(errorType.message) diff --git a/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidator.kt b/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidator.kt new file mode 100644 index 00000000..52713737 --- /dev/null +++ b/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidator.kt @@ -0,0 +1,33 @@ +package org.grida.validator.requestvalidator + +import org.grida.error.ErrorType +import org.grida.error.GridaException + +object RequestValidator { + + fun validate( + errorType: ErrorType, + condition: () -> Boolean + ) { + if (!condition.invoke()) { + throw GridaException(errorType) + } + } + + fun validate( + errorCode: String, + errorMessage: String, + condition: () -> Boolean + ) { + if (!condition.invoke()) { + val errorType = InvalidInputValue(errorCode, errorMessage) + throw GridaException(errorType) + } + } + + fun validate(condition: () -> Boolean) { + if (!condition.invoke()) { + throw GridaException(DefaultInvalidInputValue) + } + } +} diff --git a/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidatorErrorType.kt b/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidatorErrorType.kt new file mode 100644 index 00000000..ffb989f1 --- /dev/null +++ b/grida-common/src/main/kotlin/org/grida/validator/requestvalidator/RequestValidatorErrorType.kt @@ -0,0 +1,22 @@ +package org.grida.validator.requestvalidator + +import org.grida.error.ErrorType +import org.grida.error.INFO +import org.grida.error.LogLevel +import org.grida.http.BAD_REQUEST + +sealed interface RequestValidatorErrorType : ErrorType + +data class InvalidInputValue( + override val errorCode: String, + override val message: String, + override val httpStatusCode: Int = BAD_REQUEST, + override val logLevel: LogLevel = INFO +) : RequestValidatorErrorType + +data object DefaultInvalidInputValue : RequestValidatorErrorType { + override val httpStatusCode: Int = BAD_REQUEST + override val errorCode: String = "HTTP_400_0" + override val message: String = "올바르지 않은 입력값 입니다." + override val logLevel: LogLevel = INFO +} diff --git a/grida-core/core-api/build.gradle.kts b/grida-core/core-api/build.gradle.kts index 7bf7c1a4..7095f648 100644 --- a/grida-core/core-api/build.gradle.kts +++ b/grida-core/core-api/build.gradle.kts @@ -6,6 +6,7 @@ dependencies { // module dependencies implementation(project(":grida-core:core-domain")) implementation(project(":grida-database:database-rds")) + implementation(project(":grida-clients:kakao-client")) // web implementation("org.springframework.boot:spring-boot-starter-web") @@ -16,6 +17,9 @@ dependencies { // security implementation("com.github.wwan13:winter-security:0.0.10") + // logging-request + implementation("com.github.wwan13:spring-request-logger:0.0.3") + // api docs testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") testImplementation("com.github.wwan13.kotlin-dsl-rest-docs:impl-mockmvc:1.2.9") diff --git a/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokenProvider.kt b/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokenProvider.kt deleted file mode 100644 index 06a1df3f..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokenProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.grida.auth - -import io.wwan13.wintersecurity.jwt.TokenGenerator -import io.wwan13.wintersecurity.passwordencoder.PasswordEncoder -import org.grida.domain.user.UserService -import org.grida.error.GridaException -import org.grida.error.LoginFailed -import org.springframework.stereotype.Component - -@Component -class AuthTokenProvider( - private val tokenGenerator: TokenGenerator, - private val userService: UserService, - private val passwordEncoder: PasswordEncoder -) { - - fun provide( - username: String, - password: String - ): AuthTokens { - val user = userService.findUser(username) - if (!passwordEncoder.matches(password, user.password)) throw GridaException(LoginFailed) - - val tokenPayload = TokenPayload(user.id, user.role) - return AuthTokens( - accessToken = tokenGenerator.accessToken(tokenPayload), - refreshToken = tokenGenerator.refreshToken(tokenPayload) - ) - } -} diff --git a/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokens.kt b/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokens.kt deleted file mode 100644 index e9ca2a60..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/auth/AuthTokens.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.grida.auth - -data class AuthTokens( - val accessToken: String, - val refreshToken: String, -) diff --git a/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiConfig.kt b/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiConfig.kt index 35905ebf..65ad2fdd 100644 --- a/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiConfig.kt +++ b/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiConfig.kt @@ -1,10 +1,27 @@ package org.grida.config -import org.grida.support.requestlogger.EnableLogRequest +import io.wwan13.springrequestlogger.configure.EnableLoggingRequest +import io.wwan13.springrequestlogger.configure.LogMessageConfigurer +import io.wwan13.springrequestlogger.context.RequestContext import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.context.annotation.Configuration +import java.util.function.Function @Configuration @ConfigurationPropertiesScan -@EnableLogRequest -class CoreApiConfig +@EnableLoggingRequest +class CoreApiConfig : LogMessageConfigurer { + + override fun format(): Function { + return Function { context -> + """ + | + | ${context.method} '${context.uri}' - ${context.status} (${context.elapsed} s) + | >> Request Headers : ${context.requestHeaders} + | >> Request Params : ${context.requestParams} + | >> Request Body : ${context.requestBody} + | >> Response Body : ${context.responseBody} + """.trimMargin() + } + } +} diff --git a/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiSecurityConfig.kt b/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiSecurityConfig.kt index dce6506e..8ceaa490 100644 --- a/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiSecurityConfig.kt +++ b/grida-core/core-api/src/main/kotlin/org/grida/config/CoreApiSecurityConfig.kt @@ -31,8 +31,8 @@ class CoreApiSecurityConfig( override fun registerAuthPatterns(registry: AuthPatternsRegistry) { registry.apply { - uriPatterns("/api/v1/auth/login", "/api/v1/user") - .httpMethodPost() + uriPatterns("/api/v1/auth/**") + .allHttpMethods() .permitAll() uriPatterns("/api/v1/user/image/**", "/api/v1/diary/**") diff --git a/grida-core/core-api/src/main/kotlin/org/grida/auth/TokenPayload.kt b/grida-core/core-api/src/main/kotlin/org/grida/config/TokenPayload.kt similarity index 92% rename from grida-core/core-api/src/main/kotlin/org/grida/auth/TokenPayload.kt rename to grida-core/core-api/src/main/kotlin/org/grida/config/TokenPayload.kt index 39012420..fa1b90d9 100644 --- a/grida-core/core-api/src/main/kotlin/org/grida/auth/TokenPayload.kt +++ b/grida-core/core-api/src/main/kotlin/org/grida/config/TokenPayload.kt @@ -1,4 +1,4 @@ -package org.grida.auth +package org.grida.config import io.wwan13.wintersecurity.jwt.payload.annotation.Payload import io.wwan13.wintersecurity.jwt.payload.annotation.Roles diff --git a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/AuthController.kt b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/AuthController.kt index b9b5e52e..260d28f2 100644 --- a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/AuthController.kt +++ b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/AuthController.kt @@ -1,25 +1,44 @@ package org.grida.presentation.v1.auth +import io.wwan13.wintersecurity.jwt.TokenGenerator import org.grida.api.ApiResponse -import org.grida.auth.AuthTokenProvider -import org.grida.auth.AuthTokens -import org.grida.presentation.v1.auth.dto.LoginRequest -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody +import org.grida.auth.KakaoAuthClient +import org.grida.config.TokenPayload +import org.grida.domain.user.LoginOption +import org.grida.domain.user.LoginPlatform +import org.grida.domain.user.UserService +import org.grida.presentation.v1.auth.dto.LoginResponse +import org.grida.user.KakaoUserClient +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v1/auth") class AuthController( - private val authTokenProvider: AuthTokenProvider + private val userService: UserService, + private val tokenGenerator: TokenGenerator, + private val kakaoAuthClient: KakaoAuthClient, + private val kakaoUserClient: KakaoUserClient ) { - @PostMapping("/login") - fun login( - @RequestBody request: LoginRequest - ): ApiResponse { - val authTokens = authTokenProvider.provide(request.username, request.password) - return ApiResponse.success(authTokens) + @GetMapping("/kakao") + fun kakaoLogin( + @RequestParam("code") kakaoAuthCode: String + ): ApiResponse { + val kakaoToken = kakaoAuthClient.provideAuthToken(kakaoAuthCode) + val kakaoProfile = kakaoUserClient.readUserProfile(kakaoToken.accessToken) + + val loginOption = LoginOption(LoginPlatform.KAKAO, kakaoProfile.id) + val user = userService.readUserByLoginOption(loginOption) + ?: userService.appendAndReturnNormalUser(kakaoProfile.name, loginOption) + + val tokenPayload = TokenPayload(user.id, user.role) + val response = LoginResponse( + accessToken = tokenGenerator.accessToken(tokenPayload), + refreshToken = tokenGenerator.refreshToken(tokenPayload) + ) + return ApiResponse.success(response) } } diff --git a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginRequest.kt b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginRequest.kt deleted file mode 100644 index 595e1193..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.grida.presentation.v1.auth.dto - -data class LoginRequest( - val username: String, - val password: String -) diff --git a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginResponse.kt b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginResponse.kt new file mode 100644 index 00000000..80d357b6 --- /dev/null +++ b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/auth/dto/LoginResponse.kt @@ -0,0 +1,6 @@ +package org.grida.presentation.v1.auth.dto + +data class LoginResponse( + val accessToken: String, + val refreshToken: String, +) diff --git a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/UserController.kt b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/UserController.kt index fd892022..91b2b6df 100644 --- a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/UserController.kt +++ b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/UserController.kt @@ -1,36 +1,8 @@ package org.grida.presentation.v1.user -import io.wwan13.wintersecurity.passwordencoder.PasswordEncoder -import org.grida.api.ApiResponse -import org.grida.api.dto.IdResponse -import org.grida.domain.user.Role -import org.grida.domain.user.User -import org.grida.domain.user.UserService -import org.grida.presentation.v1.user.dto.SignInRequest -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/v1/user") -class UserController( - private val userService: UserService, - private val passwordEncoder: PasswordEncoder -) { - - @PostMapping - fun signIn( - @RequestBody request: SignInRequest - ): ApiResponse { - val user = User( - username = request.username, - password = passwordEncoder.encode(request.password), - nickname = request.username, - role = Role.ROLE_USER - ) - val userId = userService.appendNormalUser(user, request.passwordConfirm) - val response = IdResponse(userId) - return ApiResponse.success(response) - } -} +class UserController diff --git a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/dto/SignInRequest.kt b/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/dto/SignInRequest.kt deleted file mode 100644 index 43406b99..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/presentation/v1/user/dto/SignInRequest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.grida.presentation.v1.user.dto - -import org.grida.domain.user.User - -data class SignInRequest( - val username: String, - val password: String, - val passwordConfirm: String, - val nickname: String -) { - fun toUser(): User { - return User( - username = username, - password = password, - nickname = nickname - ) - } -} diff --git a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/EnableLogRequest.kt b/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/EnableLogRequest.kt deleted file mode 100644 index 533d5d57..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/EnableLogRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.grida.support.requestlogger - -import org.springframework.context.annotation.Import - -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.CLASS) -@Import(LogFilterRegistrar::class) -annotation class EnableLogRequest() diff --git a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilter.kt b/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilter.kt deleted file mode 100644 index 1dd9fcf7..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.grida.support.requestlogger - -import com.fasterxml.jackson.databind.ObjectMapper -import mu.KotlinLogging -import org.springframework.web.filter.OncePerRequestFilter -import org.springframework.web.util.ContentCachingRequestWrapper -import org.springframework.web.util.ContentCachingResponseWrapper -import javax.servlet.FilterChain -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -private val log = KotlinLogging.logger {} - -class LogFilter( - private val objectMapper: ObjectMapper -) : OncePerRequestFilter() { - - override fun doFilterInternal( - request: HttpServletRequest, - response: HttpServletResponse, - filterChain: FilterChain - ) { - val requestWrapper = ContentCachingRequestWrapper(request) - val responseWrapper = ContentCachingResponseWrapper(response) - - val requestOccurred = System.currentTimeMillis() - filterChain.doFilter(requestWrapper, responseWrapper) - val requestCompleted = System.currentTimeMillis() - val requestElapsed = (requestCompleted - requestOccurred) / 1000.0 - - val logContext = RequestLogContext.of(requestWrapper, responseWrapper, requestElapsed) - log.info(logContext.toLogMessage(objectMapper)) - responseWrapper.copyBodyToResponse() - } - - override fun shouldNotFilter(request: HttpServletRequest): Boolean { - return request.requestURI.startsWith("/api/actuator") - } -} diff --git a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilterRegistrar.kt b/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilterRegistrar.kt deleted file mode 100644 index 049f27b0..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/LogFilterRegistrar.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.grida.support.requestlogger - -import com.fasterxml.jackson.databind.ObjectMapper -import org.springframework.boot.web.servlet.FilterRegistrationBean -import org.springframework.context.annotation.Bean - -class LogFilterRegistrar { - - @Bean - fun logFilter( - objectMapper: ObjectMapper - ): FilterRegistrationBean { - val filterRegistration = FilterRegistrationBean() - filterRegistration.filter = LogFilter(objectMapper) - filterRegistration.order = 0 - return filterRegistration - } -} diff --git a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/RequestLogContext.kt b/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/RequestLogContext.kt deleted file mode 100644 index a0a683ab..00000000 --- a/grida-core/core-api/src/main/kotlin/org/grida/support/requestlogger/RequestLogContext.kt +++ /dev/null @@ -1,60 +0,0 @@ -package org.grida.support.requestlogger - -import com.fasterxml.jackson.databind.ObjectMapper -import org.springframework.http.HttpStatus -import org.springframework.web.util.ContentCachingRequestWrapper -import org.springframework.web.util.ContentCachingResponseWrapper -import java.util.Enumeration - -data class RequestLogContext( - val method: String, - val uri: String, - val status: HttpStatus, - val elapsed: Double, - val requestHeaders: Map, - val requestParams: Map, - val requestBody: String, - val responseBody: String -) { - - fun toLogMessage(objectMapper: ObjectMapper): String { - return """ - | - |$method $uri - $status ($elapsed s) - |>> REQUEST HEADERS : $requestHeaders - |>> REQUEST PARAMS : $requestParams - |>> REQUEST BODY : ${objectMapper.readTree(requestBody.ifBlank { "{}" })} - |>> RESPONSE BODY : ${objectMapper.readTree(responseBody)} - """.trimMargin() - } - - companion object { - fun of( - request: ContentCachingRequestWrapper, - response: ContentCachingResponseWrapper, - elapsed: Double - ): RequestLogContext { - return RequestLogContext( - method = request.method, - uri = request.requestURI, - status = HttpStatus.valueOf(response.status), - elapsed = elapsed, - requestHeaders = extractAsMap(request, request.headerNames), - requestParams = extractAsMap(request, request.parameterNames), - requestBody = String(request.contentAsByteArray), - responseBody = String(response.contentAsByteArray), - ) - } - - private fun extractAsMap( - request: ContentCachingRequestWrapper, - names: Enumeration - ): Map { - val result = mutableMapOf() - names.asIterator().forEach { - result.put(it, request.getHeader(it)) - } - return result - } - } -} diff --git a/grida-core/core-api/src/main/resources/application.yml b/grida-core/core-api/src/main/resources/application.yml index c62016ce..8f28461d 100644 --- a/grida-core/core-api/src/main/resources/application.yml +++ b/grida-core/core-api/src/main/resources/application.yml @@ -39,6 +39,10 @@ storage: bucket: ${S3_BUCKET} host: ${CDN_HOST} +kakao: + appKey: ${KAKAO_APP_KEY} + redirectUri: ${KAKAO_REDIRECT_URL} + management: endpoints: web: diff --git a/grida-core/core-api/src/test/kotlin/org/grida/docs/auth/AuthApiDocsTest.kt b/grida-core/core-api/src/test/kotlin/org/grida/docs/auth/AuthApiDocsTest.kt index effd4b5c..ec8b0a60 100644 --- a/grida-core/core-api/src/test/kotlin/org/grida/docs/auth/AuthApiDocsTest.kt +++ b/grida-core/core-api/src/test/kotlin/org/grida/docs/auth/AuthApiDocsTest.kt @@ -4,12 +4,19 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.every import io.wwan13.api.document.snippets.STRING import io.wwan13.api.document.snippets.isTypeOf +import io.wwan13.api.document.snippets.moreAbout import io.wwan13.api.document.snippets.whichMeans -import org.grida.auth.AuthTokenProvider -import org.grida.auth.AuthTokens +import io.wwan13.wintersecurity.jwt.TokenGenerator +import org.grida.auth.KakaoAuthClient +import org.grida.auth.KakaoAuthToken import org.grida.docs.ApiDocsTest +import org.grida.domain.user.LoginOption +import org.grida.domain.user.LoginPlatform +import org.grida.domain.user.User +import org.grida.domain.user.UserService import org.grida.presentation.v1.auth.AuthController -import org.grida.presentation.v1.auth.dto.LoginRequest +import org.grida.user.KakaoUserClient +import org.grida.user.KakaoUserProfile import org.junit.jupiter.api.Test import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @@ -22,30 +29,49 @@ class AuthApiDocsTest( ) { @MockkBean - private lateinit var authTokenProvider: AuthTokenProvider + private lateinit var userService: UserService + + @MockkBean + private lateinit var tokenGenerator: TokenGenerator + + @MockkBean + private lateinit var kakaoAuthClient: KakaoAuthClient + + @MockkBean + private lateinit var kakaoUserClient: KakaoUserClient @Test - fun `로그인 API`() { + fun `카카오 로그인 API`() { - every { authTokenProvider.provide(any(), any()) } returns AuthTokens( - "access token", "refresh token" + val user = User( + id = 1L, name = "김태완", loginOption = LoginOption(LoginPlatform.KAKAO, "123123") ) - val api = api.post("/api/v1/auth/login") { - requestBody( - LoginRequest( - username = "username", - password = "password" - ) - ) + every { userService.readUserByLoginOption(any()) } returns user + + every { tokenGenerator.accessToken(any()) } returns "accessToken" + every { tokenGenerator.refreshToken(any()) } returns "refreshToken" + + val kakaoAuthToken = KakaoAuthToken("kakaoAccessToken", "kakaoRefreshToken") + every { kakaoAuthClient.provideAuthToken(any()) } returns kakaoAuthToken + + val kakaoUserProfile = KakaoUserProfile("123123", "김태완") + every { kakaoUserClient.readUserProfile(any()) } returns kakaoUserProfile + + val api = api.get("/api/v1/auth/kakao") { + queryParam("code", "kakao authorization code") } - documentFor(api, "login") { - summary("로그인 API") - requestFields( - "username" isTypeOf STRING whichMeans "유저 아이디", - "password" isTypeOf STRING whichMeans "유저 비밀번호" + documentFor(api, "kakao-login") { + summary( + "카카오 로그인 API" moreAbout """ + ** local : https://kauth.kakao.com/oauth/authorize?client_id=e32f0cc35368a69966b54698b193a794& + redirect_uri=http://localhost:8080/api/v1/auth/login/kakao&response_type=code
+ ** live : https://kauth.kakao.com/oauth/authorize?client_id=e32f0cc35368a69966b54698b193a794& + redirect_uri=https://grida.today/api/v1/auth/login/kakao&response_type=code
+ """.trimIndent() ) + queryParameters("code" whichMeans "카카오 인증 토큰") responseFields( "data.accessToken" isTypeOf STRING whichMeans "인증 토큰", "data.refreshToken" isTypeOf STRING whichMeans "재발급 토큰", diff --git a/grida-core/core-api/src/test/kotlin/org/grida/docs/user/UserApiDocsTest.kt b/grida-core/core-api/src/test/kotlin/org/grida/docs/user/UserApiDocsTest.kt index e1a022c6..4ac2c8e8 100644 --- a/grida-core/core-api/src/test/kotlin/org/grida/docs/user/UserApiDocsTest.kt +++ b/grida-core/core-api/src/test/kotlin/org/grida/docs/user/UserApiDocsTest.kt @@ -1,17 +1,7 @@ package org.grida.docs.user -import com.ninjasquad.springmockk.MockkBean -import io.mockk.every -import io.wwan13.api.document.snippets.NUMBER -import io.wwan13.api.document.snippets.STRING -import io.wwan13.api.document.snippets.isTypeOf -import io.wwan13.api.document.snippets.whichMeans -import io.wwan13.wintersecurity.passwordencoder.PasswordEncoder import org.grida.docs.ApiDocsTest -import org.grida.domain.user.UserService import org.grida.presentation.v1.user.UserController -import org.grida.presentation.v1.user.dto.SignInRequest -import org.junit.jupiter.api.Test import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @WebMvcTest(controllers = [UserController::class]) @@ -20,42 +10,4 @@ class UserApiDocsTest( ) : ApiDocsTest( userController, "user" -) { - - @MockkBean - private lateinit var userService: UserService - - @MockkBean - private lateinit var passwordEncoder: PasswordEncoder - - @Test - fun `회원가입 API`() { - - every { userService.appendNormalUser(any(), any()) } returns 1L - every { passwordEncoder.encode(any()) } returns "encoded password" - - val api = api.post("/api/v1/user") { - requestBody( - SignInRequest( - username = "username", - password = "raw password", - passwordConfirm = "raw password", - nickname = "nickname" - ) - ) - } - - documentFor(api, "sign-in") { - summary("회원가입 API") - requestFields( - "username" isTypeOf STRING whichMeans "유저 아이디", - "password" isTypeOf STRING whichMeans "유저 비밀번호", - "passwordConfirm" isTypeOf STRING whichMeans "비밀번호 확인", - "nickname" isTypeOf STRING whichMeans "유저 닉네임" - ) - responseFields( - "data.id" isTypeOf NUMBER whichMeans "생성된 사용자 ID" - ) - } - } -} +) diff --git a/grida-core/core-domain/build.gradle.kts b/grida-core/core-domain/build.gradle.kts index e8ed217c..f96f9b02 100644 --- a/grida-core/core-domain/build.gradle.kts +++ b/grida-core/core-domain/build.gradle.kts @@ -1,6 +1,6 @@ dependencies { implementation("org.springframework:spring-tx:6.1.0") - implementation(project(":grida-clients:ai-client")) + implementation(project(":grida-clients:openai-client")) implementation(project(":grida-clients:storage-client")) } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/AccessManager.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/AccessManager.kt index d859627c..5ade761b 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/AccessManager.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/AccessManager.kt @@ -15,4 +15,4 @@ class AccessManager { throw GridaException(AccessFailed) } } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/BaseEnum.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/BaseEnum.kt new file mode 100644 index 00000000..71eb6ba6 --- /dev/null +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/BaseEnum.kt @@ -0,0 +1,17 @@ +package org.grida.domain.base + +import org.grida.error.GridaException +import org.grida.error.InvalidEnumValue + +interface BaseEnum> { + val value: String + + companion object { + inline fun > resolve(value: String): T { + val entries = enumValues() + return entries + .firstOrNull { (it as BaseEnum<*>).value == value } + ?: throw GridaException(InvalidEnumValue(enumValues())) + } + } +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/Ownable.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/Ownable.kt index ceea72bc..a751cee6 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/Ownable.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/base/Ownable.kt @@ -3,4 +3,4 @@ package org.grida.domain.base interface Ownable { fun isOwner(accessorId: Long): Boolean -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryModifier.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryModifier.kt index 5211f230..71b40490 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryModifier.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryModifier.kt @@ -36,4 +36,4 @@ class DiaryModifier( diaryRepository.updateScope(diaryId, scope) } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryReader.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryReader.kt index fe4c54b2..99c2417f 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryReader.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryReader.kt @@ -12,4 +12,4 @@ class DiaryReader( fun read(diaryId: Long): Diary { return diaryRepository.findById(diaryId) } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryRepository.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryRepository.kt index a92efc7c..4245a2f9 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryRepository.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryRepository.kt @@ -14,4 +14,4 @@ interface DiaryRepository { fun updateContent(diaryId: Long, content: String): Long fun updateScope(diaryId: Long, scope: DiaryScope): Long -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryScope.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryScope.kt index 80957614..f09b4f4e 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryScope.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryScope.kt @@ -2,4 +2,4 @@ package org.grida.domain.diary enum class DiaryScope { PUBLIC, FRIENDS_ONLY, PRIVATE -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryService.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryService.kt index 4a3fc74b..356e2b20 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryService.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diary/DiaryService.kt @@ -45,4 +45,4 @@ class DiaryService( diaryModifier.modifyScope(diaryId, userId, scope) return diaryId } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageAppender.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageAppender.kt index 8a23d9e2..8890a329 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageAppender.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageAppender.kt @@ -22,4 +22,4 @@ class DiaryImageAppender( val user = userReader.read(userId) return diaryImageRepository.save(diaryImage, diary, user) } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageGenerator.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageGenerator.kt index 07f95ebc..12cac7a1 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageGenerator.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageGenerator.kt @@ -9,4 +9,4 @@ class DiaryImageGenerator { // TODO return "https://tmpImageUrl.com" } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageRepository.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageRepository.kt index 274dc085..68be4ddd 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageRepository.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageRepository.kt @@ -16,4 +16,4 @@ interface DiaryImageRepository { fun existsByDiaryIdAndStatus(diaryId: Long, status: ImageStatus): Boolean fun updateStatus(diaryImageId: Long, status: ImageStatus): Long -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageService.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageService.kt index 0aa2530e..d1ab82f1 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageService.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/diaryimage/DiaryImageService.kt @@ -58,4 +58,4 @@ class DiaryImageService( diaryImageModifier.modifyOriginalDiaryImageAsDeactivate(diaryId, userId) diaryImageModifier.modifyAsActivate(diaryImageId, userId) } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImageGenerator.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImageGenerator.kt index 50d43db6..8d66a996 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImageGenerator.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImageGenerator.kt @@ -1,21 +1,19 @@ package org.grida.domain.profileimage -import org.grida.image.ImageGenerateClient -import org.grida.image.ImageGenerateRequest +import org.grida.image.OpenAiImageClient import org.springframework.stereotype.Component @Component class ProfileImageGenerator( - private val imageGenerateClient: ImageGenerateClient + private val openAiImageClient: OpenAiImageClient ) { fun generate( appearance: Appearance ): String { val prompt = ProfileImagePrompt(appearance) - val request = ImageGenerateRequest(prompt.value) - val response = imageGenerateClient.generate(request) + val imageUrl = openAiImageClient.generate(prompt.apply()) - return response.imageUrl + return imageUrl } } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImagePrompt.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImagePrompt.kt index b14ef05c..5e4763d0 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImagePrompt.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/profileimage/ProfileImagePrompt.kt @@ -4,14 +4,15 @@ data class ProfileImagePrompt( val appearance: Appearance ) { - val value = - """ - Please generate a cozy style profile illustration - for a ${appearance.age}-year-old Korean ${appearance.gender} according to the following description. - HairStyle is ${appearance.hairStyle}. - has ${appearance.glasses} - only one person looking directly front. \n" + - The background is plain white without decorations, highlighting the main subject. - This simplicity enhances the cozy and homely feel of the artwork. + fun apply(): String { + return """ + Please generate a cozy style profile illustration + for a ${appearance.age}-year-old Korean ${appearance.gender} according to the following description. + HairStyle is ${appearance.hairStyle}. + has ${appearance.glasses} + only one person looking directly front. \n" + + The background is plain white without decorations, highlighting the main subject. + This simplicity enhances the cozy and homely feel of the artwork. """.trimIndent() + } } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginOption.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginOption.kt new file mode 100644 index 00000000..1ee77362 --- /dev/null +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginOption.kt @@ -0,0 +1,6 @@ +package org.grida.domain.user + +data class LoginOption( + val platform: LoginPlatform, + val identifier: String +) diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginPlatform.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginPlatform.kt new file mode 100644 index 00000000..22c35854 --- /dev/null +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/LoginPlatform.kt @@ -0,0 +1,11 @@ +package org.grida.domain.user + +import org.grida.domain.base.BaseEnum + +enum class LoginPlatform( + override val value: String +) : BaseEnum { + KAKAO("카카오"), + GOOGLE("구글"), + GITHUB("깃허브"); +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/User.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/User.kt index 63aca0f0..1c863e1e 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/User.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/User.kt @@ -5,8 +5,7 @@ import org.grida.domain.base.Timestamp data class User( val id: Long = 0, val timestamp: Timestamp = Timestamp(), - val username: String, - val password: String, + val name: String, val role: Role = Role.ROLE_USER, - val nickname: String, + val loginOption: LoginOption ) diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserAppender.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserAppender.kt index 98c324a2..3648f62d 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserAppender.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserAppender.kt @@ -5,13 +5,17 @@ import org.springframework.transaction.annotation.Transactional @Component class UserAppender( - private val userRepository: UserRepository, - private val userValidator: UserValidator + private val userRepository: UserRepository ) { @Transactional fun append(user: User): Long { - userValidator.validateUsernameAlreadyInUse(user.username) return userRepository.save(user) } -} \ No newline at end of file + + @Transactional + fun appendAndReturnUser(user: User): User { + val userid = userRepository.save(user) + return userRepository.findById(userid) + } +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserReader.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserReader.kt index 48771fab..f64cdfe1 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserReader.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserReader.kt @@ -14,7 +14,7 @@ class UserReader( } @Transactional(readOnly = true) - fun read(username: String): User { - return userRepository.findByUsername(username) + fun readByLoginOptionOrNull(loginOption: LoginOption): User? { + return userRepository.findByLoginOption(loginOption) } -} \ No newline at end of file +} diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserRepository.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserRepository.kt index fd6d1e2c..1ee6b4e3 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserRepository.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserRepository.kt @@ -6,7 +6,7 @@ interface UserRepository { fun findById(id: Long): User - fun findByUsername(username: String): User + fun findByLoginOption(loginOption: LoginOption): User? - fun existsByUsername(username: String): Boolean + fun existsByLoginOption(loginOption: LoginOption): Boolean } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserService.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserService.kt index 03a16527..786b8ccc 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserService.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserService.kt @@ -5,19 +5,34 @@ import org.springframework.stereotype.Service @Service class UserService( private val userAppender: UserAppender, - private val userReader: UserReader, - private val userValidator: UserValidator + private val userReader: UserReader ) { fun appendNormalUser( - user: User, - passwordConfirm: String + name: String, + loginOption: LoginOption ): Long { - userValidator.validatePasswordConfirmMatches(user.password, passwordConfirm) + val user = User( + name = name, + loginOption = loginOption + ) return userAppender.append(user) } - fun findUser(username: String): User { - return userReader.read(username) + fun appendAndReturnNormalUser( + name: String, + loginOption: LoginOption + ): User { + val user = User( + name = name, + loginOption = loginOption + ) + return userAppender.appendAndReturnUser(user) + } + + fun readUserByLoginOption( + loginOption: LoginOption + ): User? { + return userReader.readByLoginOptionOrNull(loginOption) } } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserValidator.kt b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserValidator.kt index cd8b03a8..0aefe521 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserValidator.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/domain/user/UserValidator.kt @@ -1,29 +1,17 @@ package org.grida.domain.user +import org.grida.error.AlreadyRegisteredUser import org.grida.error.GridaException -import org.grida.error.PasswordConfirmNotMatched -import org.grida.error.UnusableUsername import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional @Component class UserValidator( private val userRepository: UserRepository ) { - fun validatePasswordConfirmMatches( - password: String, - passwordConfirm: String - ) { - if (password != passwordConfirm) { - throw GridaException(PasswordConfirmNotMatched) - } - } - - @Transactional(readOnly = true) - fun validateUsernameAlreadyInUse(username: String) { - if (userRepository.existsByUsername(username)) { - throw GridaException(UnusableUsername) + fun validateAlreadyRegistered(loginOption: LoginOption) { + if (userRepository.existsByLoginOption(loginOption)) { + throw GridaException(AlreadyRegisteredUser) } } } diff --git a/grida-core/core-domain/src/main/kotlin/org/grida/error/CoreDomainErrorType.kt b/grida-core/core-domain/src/main/kotlin/org/grida/error/CoreDomainErrorType.kt index 99310bdd..90e91a53 100644 --- a/grida-core/core-domain/src/main/kotlin/org/grida/error/CoreDomainErrorType.kt +++ b/grida-core/core-domain/src/main/kotlin/org/grida/error/CoreDomainErrorType.kt @@ -2,13 +2,26 @@ package org.grida.error import org.grida.http.BAD_REQUEST import org.grida.http.FORBIDDEN +import kotlin.reflect.KClass sealed interface CoreDomainErrorType : ErrorType -data object NoSuchData : CoreDomainErrorType { +data class NoSuchData( + val from: KClass, + val id: R +) : CoreDomainErrorType { override val httpStatusCode: Int = BAD_REQUEST override val errorCode: String = "CORE_DOMAIN_400_1" - override val message: String = "찾으려는 데이터가 없습니다." + override val message: String = "찾으려는 데이터가 없습니다. ($from-$id)" + override val logLevel: LogLevel = INFO +} + +class InvalidEnumValue( + enumValues: Array +) : CoreDomainErrorType { + override val httpStatusCode: Int = BAD_REQUEST + override val errorCode: String = "CORE_DOMAIN_400_2" + override val message: String = "유효하지 않은 enum value 입니다.(${enumValues.joinToString(",")}})" override val logLevel: LogLevel = INFO } @@ -19,10 +32,10 @@ data object AccessFailed : CoreDomainErrorType { override val logLevel: LogLevel = INFO } -data object UnusableUsername : CoreDomainErrorType { +data object AlreadyRegisteredUser : CoreDomainErrorType { override val httpStatusCode: Int = BAD_REQUEST override val errorCode: String = "USER_400_1" - override val message: String = "사용할 수 없는 username 입니다." + override val message: String = "이미 등록된 유저입니다." override val logLevel: LogLevel = INFO } diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityMapper.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityMapper.kt index 788d48e9..63d0e431 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityMapper.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityMapper.kt @@ -23,4 +23,4 @@ fun DiaryEntity.toDomain(): Diary { scope = this.scope, userId = this.user.id ) -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityRepository.kt index fceb660c..8fb22119 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryEntityRepository.kt @@ -49,4 +49,4 @@ class DiaryEntityRepository( diaryEntity.updateScope(scope) return diaryEntity.id } -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryJpaEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryJpaEntityRepository.kt index c8b9e7e3..dc8f30eb 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryJpaEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diary/DiaryJpaEntityRepository.kt @@ -7,10 +7,10 @@ import java.time.LocalDate fun DiaryJpaEntityRepository.findByIdOrException(id: Long): DiaryEntity { return this.findById(id) - .orElseThrow { GridaException(NoSuchData) } + .orElseThrow { GridaException(NoSuchData(DiaryEntity::class, id)) } } interface DiaryJpaEntityRepository : JpaRepository { fun existsByUserIdAndTargetDate(userId: Long, targetDate: LocalDate): Boolean -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntity.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntity.kt index d54734a4..cbed9f51 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntity.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntity.kt @@ -1,6 +1,5 @@ package org.grida.persistence.diaryimage -import org.grida.domain.diaryimage.DiaryImage import org.grida.domain.image.ImageStatus import org.grida.persistence.base.BaseEntity import org.grida.persistence.diary.DiaryEntity @@ -44,4 +43,4 @@ class DiaryImageEntity( fun updateStatue(status: ImageStatus) { this.status = status } -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityMapper.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityMapper.kt index fb5683fd..61c916d2 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityMapper.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityMapper.kt @@ -31,4 +31,4 @@ fun DiaryImageEntity.toDomain(): DiaryImage { timestamp = this.toTimeStamp() ) ) -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityRepository.kt index 6f165dd4..44922736 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageEntityRepository.kt @@ -53,4 +53,4 @@ class DiaryImageEntityRepository( diaryImageEntity.updateStatue(status) return diaryImageEntity.id } -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageJpaEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageJpaEntityRepository.kt index 5477102f..3a92e0a3 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageJpaEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/diaryimage/DiaryImageJpaEntityRepository.kt @@ -9,7 +9,8 @@ import java.util.Optional fun DiaryImageJpaEntityRepository.findByIdOrException( id: Long ): DiaryImageEntity { - return this.findById(id).orElseThrow { GridaException(NoSuchData) } + return this.findById(id) + .orElseThrow { GridaException(NoSuchData(DiaryImageEntity::class, id)) } } fun DiaryImageJpaEntityRepository.findByDiaryIdAndStatusOrException( @@ -17,7 +18,7 @@ fun DiaryImageJpaEntityRepository.findByDiaryIdAndStatusOrException( status: ImageStatus ): DiaryImageEntity { return this.findByDiaryIdAndStatus(diaryId, status) - .orElseThrow { GridaException(NoSuchData) } + .orElseThrow { GridaException(NoSuchData(DiaryImageEntity::class, diaryId)) } } interface DiaryImageJpaEntityRepository : JpaRepository { @@ -27,4 +28,4 @@ interface DiaryImageJpaEntityRepository : JpaRepository fun countByDiaryId(diaryId: Long): Long fun existsByDiaryIdAndStatus(diaryId: Long, status: ImageStatus): Boolean -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageEntityMapper.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageEntityMapper.kt index 49322e8f..3502d4a9 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageEntityMapper.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageEntityMapper.kt @@ -36,4 +36,4 @@ fun ProfileImageEntity.toDomain(): ProfileImage { bodyShape = this.bodyShape ) ) -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageJpaEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageJpaEntityRepository.kt index c47c602b..c2707f3e 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageJpaEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/profileimage/ProfileImageJpaEntityRepository.kt @@ -8,7 +8,7 @@ import java.util.Optional fun ProfileImageJpaEntityRepository.findByIdOrException(id: Long): ProfileImageEntity { return this.findById(id) - .orElseThrow { GridaException(NoSuchData) } + .orElseThrow { GridaException(NoSuchData(ProfileImageEntity::class, id)) } } fun ProfileImageJpaEntityRepository.findByUserIdAndStatusOrException( @@ -16,7 +16,7 @@ fun ProfileImageJpaEntityRepository.findByUserIdAndStatusOrException( status: ImageStatus ): ProfileImageEntity { return this.findByUserIdAndStatus(userId, status) - .orElseThrow { GridaException(NoSuchData) } + .orElseThrow { GridaException(NoSuchData(ProfileImageEntity::class, userId)) } } interface ProfileImageJpaEntityRepository : JpaRepository { diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntity.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntity.kt index c8119ced..93160d46 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntity.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntity.kt @@ -1,7 +1,7 @@ package org.grida.persistence.user +import org.grida.domain.user.LoginPlatform import org.grida.domain.user.Role -import org.grida.domain.user.User import org.grida.persistence.base.BaseEntity import javax.persistence.Column import javax.persistence.Entity @@ -20,13 +20,13 @@ class UserEntity( @Column(name = "user_id") var id: Long = 0, - var username: String, - - @Column(name = "encrypted_password") - var password: String, + var name: String, @Enumerated(EnumType.STRING) var role: Role, - var nickname: String + @Enumerated(EnumType.STRING) + val platform: LoginPlatform, + + val platformIdentifier: String ) : BaseEntity() diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityMapper.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityMapper.kt index a348bb22..7c3c4b52 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityMapper.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityMapper.kt @@ -1,24 +1,24 @@ package org.grida.persistence.user +import org.grida.domain.user.LoginOption import org.grida.domain.user.User fun User.toEntity(): UserEntity { return UserEntity( id = this.id, - username = this.username, - password = this.password, role = this.role, - nickname = this.nickname + name = this.name, + platform = this.loginOption.platform, + platformIdentifier = this.loginOption.identifier, ) } fun UserEntity.toDomain(): User { return User( id = this.id, - username = this.username, - password = this.password, role = this.role, - nickname = this.nickname, - timestamp = this.toTimeStamp() + name = this.name, + timestamp = this.toTimeStamp(), + loginOption = LoginOption(this.platform, this.platformIdentifier) ) -} \ No newline at end of file +} diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityRepository.kt index a4cff1b5..9bc3b5f4 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserEntityRepository.kt @@ -1,5 +1,6 @@ package org.grida.persistence.user +import org.grida.domain.user.LoginOption import org.grida.domain.user.User import org.grida.domain.user.UserRepository import org.springframework.stereotype.Repository @@ -23,12 +24,15 @@ class UserEntityRepository( return userEntity.toDomain() } - override fun findByUsername(username: String): User { - val userEntity = userJpaEntityRepository.findByUsernameOrException(username) - return userEntity.toDomain() + override fun findByLoginOption(loginOption: LoginOption): User? { + val userEntity = userJpaEntityRepository + .findByPlatformAndPlatformIdentifierOrNull(loginOption.platform, loginOption.identifier) + + return userEntity?.toDomain() } - override fun existsByUsername(username: String): Boolean { - return userJpaEntityRepository.existsByUsername(username) + override fun existsByLoginOption(loginOption: LoginOption): Boolean { + return userJpaEntityRepository + .existsByPlatformAndPlatformIdentifier(loginOption.platform, loginOption.identifier) } } diff --git a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserJpaEntityRepository.kt b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserJpaEntityRepository.kt index 2528c166..6a2d6314 100644 --- a/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserJpaEntityRepository.kt +++ b/grida-database/database-rds/src/main/kotlin/org/grida/persistence/user/UserJpaEntityRepository.kt @@ -1,5 +1,6 @@ package org.grida.persistence.user +import org.grida.domain.user.LoginPlatform import org.grida.error.GridaException import org.grida.error.NoSuchData import org.springframework.data.jpa.repository.JpaRepository @@ -7,17 +8,26 @@ import java.util.Optional fun UserJpaEntityRepository.findByIdOrException(id: Long): UserEntity { return this.findById(id) - .orElseThrow { throw GridaException(NoSuchData) } + .orElseThrow { throw GridaException(NoSuchData(UserEntity::class, id)) } } -fun UserJpaEntityRepository.findByUsernameOrException(username: String): UserEntity { - return this.findByUsername(username) - .orElseThrow { throw GridaException(NoSuchData) } +fun UserJpaEntityRepository.findByPlatformAndPlatformIdentifierOrNull( + platform: LoginPlatform, + platformIdentifier: String +): UserEntity? { + return this.findByPlatformAndPlatformIdentifier(platform, platformIdentifier) + .orElse(null) } interface UserJpaEntityRepository : JpaRepository { - fun findByUsername(username: String): Optional + fun findByPlatformAndPlatformIdentifier( + platform: LoginPlatform, + platformIdentifier: String + ): Optional - fun existsByUsername(username: String): Boolean + fun existsByPlatformAndPlatformIdentifier( + platform: LoginPlatform, + platformIdentifier: String + ): Boolean } diff --git a/settings.gradle.kts b/settings.gradle.kts index 153b77cf..7abec8fb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,8 @@ rootProject.name = "grida" include("grida-clients") include("grida-clients:storage-client") -include("grida-clients:ai-client") +include("grida-clients:openai-client") +include("grida-clients:kakao-client") include("grida-core") include("grida-core:core-api")