Skip to content

Commit 6471128

Browse files
authored
Merge pull request #30 from prgrms-web-devcourse-final-project/QUZ-88-user-api
[QUZ-88][FEATURE] user-service api 작성
2 parents 6b173d0 + 7b879da commit 6471128

File tree

27 files changed

+413
-82
lines changed

27 files changed

+413
-82
lines changed

build.gradle.kts

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,85 @@
11
import org.springframework.boot.gradle.tasks.bundling.BootJar
22

33
plugins {
4-
kotlin("jvm") version "1.9.25"
5-
kotlin("plugin.spring") version "1.9.25" apply false
6-
id("org.springframework.boot") version "3.3.5" apply false
7-
id("io.spring.dependency-management") version "1.1.6" apply false
8-
id("com.diffplug.spotless") version "7.0.0.BETA4"
4+
kotlin("jvm") version "1.9.25"
5+
kotlin("plugin.spring") version "1.9.25" apply false
6+
id("org.springframework.boot") version "3.3.5" apply false
7+
id("io.spring.dependency-management") version "1.1.6" apply false
8+
id("com.diffplug.spotless") version "7.0.0.BETA4"
99
}
1010

1111
group = "com.grepp"
1212
version = "0.0.1"
1313

1414
tasks.named<Jar>("jar") {
15-
enabled = true
15+
enabled = true
1616
}
1717

1818
allprojects {
19-
group = "com.grepp.quizy"
20-
version = "0.0.1"
21-
22-
repositories {
23-
mavenCentral()
24-
gradlePluginPortal()
25-
maven {
26-
url = uri("https://packages.confluent.io/maven")
27-
}
28-
}
19+
group = "com.grepp.quizy"
20+
version = "0.0.1"
21+
22+
repositories {
23+
mavenCentral()
24+
gradlePluginPortal()
25+
maven {
26+
url = uri("https://packages.confluent.io/maven")
27+
}
28+
}
2929
}
3030

3131

3232
java {
33-
toolchain {
34-
languageVersion = JavaLanguageVersion.of(17)
35-
}
33+
toolchain {
34+
languageVersion = JavaLanguageVersion.of(17)
35+
}
3636
}
3737

3838
subprojects {
39-
apply(plugin = "org.jetbrains.kotlin.jvm")
40-
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
41-
apply(plugin = "org.springframework.boot")
42-
apply(plugin = "io.spring.dependency-management")
43-
apply(plugin = "com.diffplug.spotless")
44-
45-
dependencies {
46-
implementation("org.springframework.boot:spring-boot-starter")
47-
implementation("org.jetbrains.kotlin:kotlin-reflect")
48-
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
49-
50-
testImplementation("org.springframework.boot:spring-boot-starter-test")
51-
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
52-
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
53-
54-
// kotest
55-
testImplementation("io.kotest:kotest-runner-junit5:5.9.0")
56-
testImplementation("io.kotest:kotest-assertions-core:5.9.0")
57-
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
58-
// mockk
59-
testImplementation("io.mockk:mockk:1.13.13")
60-
61-
//logging
62-
implementation("io.github.oshai:kotlin-logging-jvm:5.1.1")
63-
}
64-
65-
tasks.named<BootJar>("bootJar") {
66-
enabled = false
67-
}
68-
69-
tasks.withType<Test> {
70-
useJUnitPlatform()
71-
}
39+
apply(plugin = "org.jetbrains.kotlin.jvm")
40+
apply(plugin = "org.jetbrains.kotlin.plugin.spring")
41+
apply(plugin = "org.springframework.boot")
42+
apply(plugin = "io.spring.dependency-management")
43+
apply(plugin = "com.diffplug.spotless")
44+
45+
dependencies {
46+
implementation("org.springframework.boot:spring-boot-starter")
47+
implementation("org.jetbrains.kotlin:kotlin-reflect")
48+
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
49+
50+
testImplementation("org.springframework.boot:spring-boot-starter-test")
51+
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
52+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
53+
54+
// kotest
55+
testImplementation("io.kotest:kotest-runner-junit5:5.9.0")
56+
testImplementation("io.kotest:kotest-assertions-core:5.9.0")
57+
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
58+
// mockk
59+
testImplementation("io.mockk:mockk:1.13.13")
60+
61+
//logging
62+
implementation("io.github.oshai:kotlin-logging-jvm:5.1.1")
63+
64+
// coroutines
65+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
66+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
67+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j")
68+
}
69+
70+
tasks.named<BootJar>("bootJar") {
71+
enabled = false
72+
}
73+
74+
tasks.withType<Test> {
75+
useJUnitPlatform()
76+
}
7277
}
7378

7479
kotlin {
75-
compilerOptions {
76-
freeCompilerArgs.addAll("-Xjsr305=strict")
77-
}
80+
compilerOptions {
81+
freeCompilerArgs.addAll("-Xjsr305=strict")
82+
}
7883
}
7984

8085

gateway-service/src/main/kotlin/com/grepp/quizy/global/AuthGlobalFilter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class RouteValidator {
107107
"/oauth2",
108108
"/api/quiz/feed",
109109
"/api/quiz/search",
110+
"/api/user/info/"
110111
)
111112

112113
fun isUnsecured(request: ServerHttpRequest): Boolean {

user-service/user-application/app-api/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ dependencies {
2222

2323
// OAuth2
2424
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
25-
25+
2626
}
2727

2828
tasks.named<BootJar>("bootJar") {

user-service/user-application/app-api/src/main/kotlin/com/grepp/quizy/user/api/HealthCheckApi.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package com.grepp.quizy.user.api
22

33
import com.grepp.quizy.common.api.ApiResponse
4+
import com.grepp.quizy.web.annotation.AuthUser
5+
import com.grepp.quizy.web.dto.UserPrincipal
46
import org.springframework.web.bind.annotation.GetMapping
5-
import org.springframework.web.bind.annotation.RequestHeader
67
import org.springframework.web.bind.annotation.RequestMapping
78
import org.springframework.web.bind.annotation.RestController
89

@@ -12,10 +13,10 @@ class HealthCheckApi {
1213

1314
@GetMapping("/health")
1415
fun healthCheck(
15-
@RequestHeader("X-Auth-Id") userId: String
16+
@AuthUser principal: UserPrincipal
1617
): ApiResponse<Unit> {
1718
return ApiResponse.success(
18-
"I'm USER service. UserId : ${userId.toLong()}"
19+
"I'm USER service. UserId : ${principal.value}"
1920
)
2021
}
2122

user-service/user-application/app-api/src/main/kotlin/com/grepp/quizy/user/api/auth/AuthApi.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.grepp.quizy.user.api.global.util.CookieUtils
66
import com.grepp.quizy.user.domain.user.UserId
77
import com.grepp.quizy.user.domain.user.UserLogoutUseCase
88
import com.grepp.quizy.user.domain.user.UserReissueUseCase
9+
import com.grepp.quizy.web.annotation.AuthUser
10+
import com.grepp.quizy.web.dto.UserPrincipal
911
import jakarta.servlet.http.HttpServletRequest
1012
import jakarta.servlet.http.HttpServletResponse
1113
import org.springframework.web.bind.annotation.*
@@ -20,21 +22,21 @@ class AuthApi(
2022
@GetMapping("/logout")
2123
fun logout(
2224
@RequestHeader("Authorization") accessToken: String,
23-
@RequestHeader("X-Auth-Id") userId: String,
25+
@AuthUser principal: UserPrincipal,
2426
request: HttpServletRequest,
2527
response: HttpServletResponse
2628
): ApiResponse<Unit> {
27-
userLogoutUseCase.logout(UserId(userId.toLong()), accessToken.substring(7))
29+
userLogoutUseCase.logout(UserId(principal.value), accessToken.substring(7))
2830
CookieUtils.deleteCookie(request, response, "refreshToken")
2931
return ApiResponse.success()
3032
}
3133

3234
@PostMapping("/reissue")
3335
fun reissue(
34-
@RequestHeader("X-Auth-Id") userId: String,
36+
@AuthUser principal: UserPrincipal,
3537
response: HttpServletResponse
3638
): ApiResponse<TokenResponse> {
37-
val reissueToken = userReissueUseCase.reissue(UserId(userId.toLong()))
39+
val reissueToken = userReissueUseCase.reissue(UserId(principal.value))
3840
CookieUtils.addCookie(
3941
response,
4042
"refreshToken",

user-service/user-application/app-api/src/main/kotlin/com/grepp/quizy/user/api/user/UserApi.kt

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,56 @@ package com.grepp.quizy.user.api.user
33
import com.grepp.quizy.common.api.ApiResponse
44
import com.grepp.quizy.user.api.global.util.toImageFile
55
import com.grepp.quizy.user.api.user.dto.UpdateProfileRequest
6+
import com.grepp.quizy.user.api.user.dto.UserResponse
7+
import com.grepp.quizy.user.domain.user.UserDeleteUseCase
68
import com.grepp.quizy.user.domain.user.UserId
9+
import com.grepp.quizy.user.domain.user.UserReadUseCase
710
import com.grepp.quizy.user.domain.user.UserUpdateUseCase
11+
import com.grepp.quizy.web.annotation.AuthUser
12+
import com.grepp.quizy.web.dto.UserPrincipal
813
import org.springframework.web.bind.annotation.*
914
import org.springframework.web.multipart.MultipartFile
1015

1116
@RestController
1217
@RequestMapping("/api/user")
1318
class UserApi(
19+
private val userReadUseCase: UserReadUseCase,
1420
private val userUpdateUseCase: UserUpdateUseCase,
21+
private val userDeleteUseCase: UserDeleteUseCase
1522
) {
23+
@GetMapping("/me")
24+
suspend fun getMe(
25+
@AuthUser principal: UserPrincipal,
26+
): ApiResponse<UserResponse> {
27+
return ApiResponse.success(UserResponse.from(userReadUseCase.getUserInfo(UserId(principal.value))))
28+
}
29+
30+
@GetMapping("/info/{userId}")
31+
suspend fun getUser(
32+
@PathVariable userId: Long,
33+
): ApiResponse<UserResponse> {
34+
return ApiResponse.success(UserResponse.from(userReadUseCase.getUserInfo(UserId(userId))))
35+
}
1636

1737
@PutMapping("/me")
1838
fun updateMe(
1939
@RequestPart("data", required = false) request: UpdateProfileRequest?,
2040
@RequestPart("profileImage", required = false) profileImage: MultipartFile?,
21-
@RequestHeader("X-Auth-Id") userId: String,
41+
@AuthUser principal: UserPrincipal,
2242
): ApiResponse<Unit> {
2343
val image = profileImage?.let {
2444
toImageFile(it)
2545
}
2646

27-
userUpdateUseCase.updateProfile(UserId(userId.toLong()), request?.name, image)
47+
userUpdateUseCase.updateProfile(UserId(principal.value), request?.name, image)
48+
return ApiResponse.success()
49+
}
50+
51+
@DeleteMapping("/me")
52+
fun deleteMe(
53+
@AuthUser principal: UserPrincipal,
54+
): ApiResponse<Unit> {
55+
userDeleteUseCase.removeUser(UserId(principal.value))
2856
return ApiResponse.success()
2957
}
3058

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.grepp.quizy.user.api.user.dto
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude
4+
import com.grepp.quizy.user.domain.user.AuthProvider
5+
import com.grepp.quizy.user.domain.user.UserInfo
6+
7+
@JsonInclude(JsonInclude.Include.NON_NULL)
8+
data class UserResponse(
9+
val id: Long,
10+
val email: String,
11+
val name: String,
12+
val profileImageUrl: String,
13+
val provider: AuthProvider,
14+
val role: String,
15+
val rating: Int?,
16+
val solvedProblems: Int?,
17+
val correctAnswerRate: Double?,
18+
val achievements: List<String>?
19+
) {
20+
companion object {
21+
fun from(userInfo: UserInfo): UserResponse {
22+
return UserResponse(
23+
id = userInfo.id,
24+
email = userInfo.email,
25+
name = userInfo.name,
26+
profileImageUrl = userInfo.profileImageUrl,
27+
provider = userInfo.provider,
28+
role = userInfo.role,
29+
rating = userInfo.rating,
30+
solvedProblems = userInfo.solvedProblems,
31+
correctAnswerRate = userInfo.correctAnswerRate,
32+
achievements = userInfo.achievements
33+
)
34+
}
35+
}
36+
}

user-service/user-application/app-api/src/main/resources/application.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ jwt:
5050
access-token-validity: 3600000 # 1시간
5151
refresh-token-validity: 604800000 # 7일
5252

53+
springdoc:
54+
api-docs:
55+
version: openapi_3_1
56+
enabled: true
57+
path: api/user/api-docs
58+
enable-spring-security: true
59+
5360
server:
5461
port: 8085
5562

@@ -101,7 +108,7 @@ springdoc:
101108
api-docs:
102109
version: openapi_3_1
103110
enabled: true
104-
path: /api-docs
111+
path: api/user/api-docs
105112
enable-spring-security: true
106113

107114
logging:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.grepp.quizy.user.domain.game
2+
3+
interface GameClient {
4+
fun getUserRating(userId: Long): UserRating
5+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.grepp.quizy.user.domain.game
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.withContext
5+
import org.slf4j.LoggerFactory
6+
import org.springframework.stereotype.Component
7+
8+
@Component
9+
class RatingReader(
10+
private val gameClient: GameClient,
11+
) {
12+
private val logger = LoggerFactory.getLogger(RatingReader::class.java)
13+
14+
suspend fun getRating(userId: Long): UserRating? {
15+
return try {
16+
withContext(Dispatchers.IO) {
17+
gameClient.getUserRating(userId)
18+
}
19+
} catch (ex: Exception) {
20+
logger.error("Failed to fetch scores for user $userId", ex)
21+
null
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)