From c54c85ba9b37e92f08894b61432e6fff16c799cd Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 17:15:23 +0900 Subject: [PATCH 001/178] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=20dto=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/UpdateUserProfileRequest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java b/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java index 5ec35926..4c483e77 100644 --- a/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java +++ b/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java @@ -1,9 +1,14 @@ package kpring.user.dto.request; import lombok.Builder; +import org.springframework.web.multipart.MultipartFile; @Builder public record UpdateUserProfileRequest( - String email + String email, + String username, + String password, + String newPassword ) { -} + +} \ No newline at end of file From 0c8fd42867bec6e6f95905d4c6955fceb7684a3d Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 17:15:39 +0900 Subject: [PATCH 002/178] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9D=91=EB=8B=B5=20dto=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/dto/response/UpdateUserProfileResponse.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user/src/main/java/kpring/user/dto/response/UpdateUserProfileResponse.java b/user/src/main/java/kpring/user/dto/response/UpdateUserProfileResponse.java index 3da68ef1..5e5779d6 100644 --- a/user/src/main/java/kpring/user/dto/response/UpdateUserProfileResponse.java +++ b/user/src/main/java/kpring/user/dto/response/UpdateUserProfileResponse.java @@ -4,6 +4,7 @@ @Builder public record UpdateUserProfileResponse( - String email + String email, + String username ) { -} +} \ No newline at end of file From 7d66b16be5428798417ba9469562e41c8691525f Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 17:27:17 +0900 Subject: [PATCH 003/178] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC,=20?= =?UTF-8?q?=EB=8B=89=EB=84=A4=EC=9E=84,=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/entity/User.kt | 10 ++++++++++ .../kotlin/kpring/user/service/UserServiceImpl.kt | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/user/src/main/kotlin/kpring/user/entity/User.kt b/user/src/main/kotlin/kpring/user/entity/User.kt index b3c5d763..73819217 100644 --- a/user/src/main/kotlin/kpring/user/entity/User.kt +++ b/user/src/main/kotlin/kpring/user/entity/User.kt @@ -1,6 +1,7 @@ package kpring.user.entity import jakarta.persistence.* +import kpring.user.dto.request.UpdateUserProfileRequest @Entity @Table(name = "tb_user") @@ -34,4 +35,13 @@ class User( followers.remove(follower) follower.followees.remove(this) } + + fun updateInfo( + request: UpdateUserProfileRequest, + newPassword: String?, + ) { + request.email?.let { this.email = it } + request.username?.let { this.username = it } + newPassword?.let { this.password = it } + } } diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index 9ab4893a..a1652671 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -32,7 +32,20 @@ class UserServiceImpl( userId: Long, request: UpdateUserProfileRequest, ): UpdateUserProfileResponse { - TODO("Not yet implemented") + val user = + userRepository.findById(userId) + .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } + lateinit var newPassword: String + + request.email?.let { handleDuplicateEmail(it) } + request.newPassword?.let { + userValidationService.validateUserPassword(request.password, user.password) + newPassword = passwordEncoder.encode(it) + } + + user.updateInfo(request, newPassword) + + return UpdateUserProfileResponse(user.email, user.username) } override fun exitUser(userId: Long): Boolean { From f639497b18cf6744478276833a4b7b9f1211383a Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 17:28:59 +0900 Subject: [PATCH 004/178] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20getUser=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/service/UserServiceImpl.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index a1652671..dd94dd13 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -21,10 +21,7 @@ class UserServiceImpl( private val userValidationService: UserValidationService, ) : UserService { override fun getProfile(userId: Long): GetUserProfileResponse { - val user = - userRepository.findById(userId) - .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } - + val user = getUser(userId) return GetUserProfileResponse(user.id, user.email, user.username) } @@ -32,9 +29,7 @@ class UserServiceImpl( userId: Long, request: UpdateUserProfileRequest, ): UpdateUserProfileResponse { - val user = - userRepository.findById(userId) - .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } + val user = getUser(userId) lateinit var newPassword: String request.email?.let { handleDuplicateEmail(it) } @@ -69,9 +64,14 @@ class UserServiceImpl( return CreateUserResponse(user.id, user.email) } - fun handleDuplicateEmail(email: String) { + private fun handleDuplicateEmail(email: String) { if (userRepository.existsByEmail(email)) { throw ServiceException(UserErrorCode.ALREADY_EXISTS_EMAIL) } } + + private fun getUser(userId: Long): User { + return userRepository.findById(userId) + .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } + } } From d6f73f3b2a94492f0d8c741a3696ee680a1ff3c4 Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 18:02:44 +0900 Subject: [PATCH 005/178] =?UTF-8?q?feat:=20application.yml=EC=97=90=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=EC=82=AC=EC=A7=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/resources/application.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user/src/main/resources/application.yaml b/user/src/main/resources/application.yaml index 68b06624..c635f869 100644 --- a/user/src/main/resources/application.yaml +++ b/user/src/main/resources/application.yaml @@ -14,3 +14,7 @@ spring: auth: url: ${AUTH_SERVICE_URL:http://localhost:8080/api/v1} + +file: + path: + profile-dir: user/src/main/resources/static/images/profileImg \ No newline at end of file From db5af5a7cb2cfcb427021087490523e8546c89cb Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 18:04:37 +0900 Subject: [PATCH 006/178] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=99=95=EC=9E=A5=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt index d1270bc4..dc5dbeec 100644 --- a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt +++ b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt @@ -10,11 +10,12 @@ enum class UserErrorCode( ) : ErrorCode { BAD_REQUEST(HttpStatus.BAD_REQUEST, "4000", "Invalid email"), ALREADY_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "4001", "Email already exists"), - NOT_MATCH_PASSWORD(HttpStatus.BAD_REQUEST, "4002", "Password does not match"), NOT_ALLOWED(HttpStatus.FORBIDDEN, "4003", "Not allowed"), // 권한이 없는 경우 INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "4010", "Incorrect password"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "4011", "User not found"), + + EXTENSION_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "4020", "Extension is not supported"), ; override fun message(): String = this.message From 26c804b7720ac23240518a5f7ed891360cb05d26 Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 18:06:13 +0900 Subject: [PATCH 007/178] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/controller/UserController.kt | 6 ++-- .../kotlin/kpring/user/service/UserService.kt | 2 ++ .../kpring/user/service/UserServiceImpl.kt | 36 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/controller/UserController.kt b/user/src/main/kotlin/kpring/user/controller/UserController.kt index 4c79c421..ba18bc86 100644 --- a/user/src/main/kotlin/kpring/user/controller/UserController.kt +++ b/user/src/main/kotlin/kpring/user/controller/UserController.kt @@ -14,6 +14,7 @@ import kpring.user.service.UserService import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/v1") @@ -43,12 +44,13 @@ class UserController( fun updateUserProfile( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, - @RequestBody request: UpdateUserProfileRequest, + @RequestPart(value = "json") request: UpdateUserProfileRequest, + @RequestPart(value = "file") multipartFile: MultipartFile, ): ResponseEntity> { val validatedUserId = checkIfAccessTokenAndGetUserId(token) checkIfUserIsSelf(userId.toString(), validatedUserId) - val response = userService.updateProfile(userId, request) + val response = userService.updateProfile(userId, request, multipartFile) return ResponseEntity.ok(ApiResponse(data = response)) } diff --git a/user/src/main/kotlin/kpring/user/service/UserService.kt b/user/src/main/kotlin/kpring/user/service/UserService.kt index 53804828..7586102e 100644 --- a/user/src/main/kotlin/kpring/user/service/UserService.kt +++ b/user/src/main/kotlin/kpring/user/service/UserService.kt @@ -5,6 +5,7 @@ import kpring.user.dto.request.UpdateUserProfileRequest import kpring.user.dto.response.CreateUserResponse import kpring.user.dto.response.GetUserProfileResponse import kpring.user.dto.response.UpdateUserProfileResponse +import org.springframework.web.multipart.MultipartFile interface UserService { fun getProfile(userId: Long): GetUserProfileResponse @@ -12,6 +13,7 @@ interface UserService { fun updateProfile( userId: Long, request: UpdateUserProfileRequest, + multipartFile: MultipartFile, ): UpdateUserProfileResponse fun exitUser(userId: Long): Boolean diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index dd94dd13..e385ffc3 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -9,9 +9,14 @@ import kpring.user.dto.response.UpdateUserProfileResponse import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.repository.UserRepository +import org.springframework.beans.factory.annotation.Value import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths @Service @Transactional @@ -20,6 +25,9 @@ class UserServiceImpl( private val passwordEncoder: PasswordEncoder, private val userValidationService: UserValidationService, ) : UserService { + @Value("\${file.path.profile-dir}") + private lateinit var profileImgDirPath: String + override fun getProfile(userId: Long): GetUserProfileResponse { val user = getUser(userId) return GetUserProfileResponse(user.id, user.email, user.username) @@ -28,9 +36,11 @@ class UserServiceImpl( override fun updateProfile( userId: Long, request: UpdateUserProfileRequest, + multipartFile: MultipartFile, ): UpdateUserProfileResponse { + var newPassword: String? = null + val profileImgDir = Paths.get(System.getProperty("user.dir")).resolve(profileImgDirPath) val user = getUser(userId) - lateinit var newPassword: String request.email?.let { handleDuplicateEmail(it) } request.newPassword?.let { @@ -38,6 +48,9 @@ class UserServiceImpl( newPassword = passwordEncoder.encode(it) } + if (!multipartFile.isEmpty) { + saveUploadedFile(multipartFile, profileImgDir) + } user.updateInfo(request, newPassword) return UpdateUserProfileResponse(user.email, user.username) @@ -74,4 +87,25 @@ class UserServiceImpl( return userRepository.findById(userId) .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } } + + private fun saveUploadedFile( + multipartFile: MultipartFile, + dirPath: Path, + ) { + if (Files.notExists(dirPath)) { + Files.createDirectories(dirPath) + } + val extension = multipartFile.originalFilename!!.substringAfterLast('.') + isFileExtensionSupported(extension) + + val filePath = dirPath.resolve(multipartFile.originalFilename!!) + multipartFile.transferTo(filePath.toFile()) + } + + private fun isFileExtensionSupported(extension: String) { + val supportedExtensions = listOf("png", "jpg", "jpeg") + if (extension !in supportedExtensions) { + throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) + } + } } From 7c7f1d512046d9d45aaf8a9879d37750e8381228 Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 18:11:00 +0900 Subject: [PATCH 008/178] =?UTF-8?q?feat:=20generateUniqueFileName=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/service/UserServiceImpl.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index e385ffc3..3839ea52 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -17,6 +17,8 @@ import org.springframework.web.multipart.MultipartFile import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import java.text.SimpleDateFormat +import java.util.* @Service @Transactional @@ -49,7 +51,7 @@ class UserServiceImpl( } if (!multipartFile.isEmpty) { - saveUploadedFile(multipartFile, profileImgDir) + saveUploadedFile(multipartFile, userId, profileImgDir) } user.updateInfo(request, newPassword) @@ -90,6 +92,7 @@ class UserServiceImpl( private fun saveUploadedFile( multipartFile: MultipartFile, + userId: Long, dirPath: Path, ) { if (Files.notExists(dirPath)) { @@ -98,7 +101,8 @@ class UserServiceImpl( val extension = multipartFile.originalFilename!!.substringAfterLast('.') isFileExtensionSupported(extension) - val filePath = dirPath.resolve(multipartFile.originalFilename!!) + val uniqueFileName = generateUniqueFileName(userId, extension) + val filePath = dirPath.resolve(uniqueFileName) multipartFile.transferTo(filePath.toFile()) } @@ -108,4 +112,12 @@ class UserServiceImpl( throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) } } + + private fun generateUniqueFileName( + userId: Long, + extension: String, + ): String { + val timeStamp = SimpleDateFormat("yyMMddHHmmss").format(Date()) + return "$timeStamp$userId.$extension" + } } From 41b33ecf7f0be938ac5f62e0dff73deaa011e416 Mon Sep 17 00:00:00 2001 From: minahYu Date: Thu, 23 May 2024 18:20:25 +0900 Subject: [PATCH 009/178] =?UTF-8?q?feat:=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20D?= =?UTF-8?q?B=EC=97=90=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=9B=84,?= =?UTF-8?q?=20=EC=9D=B4=EC=A0=84=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/entity/User.kt | 6 ++++++ .../kotlin/kpring/user/service/UserServiceImpl.kt | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/User.kt b/user/src/main/kotlin/kpring/user/entity/User.kt index 73819217..0854691b 100644 --- a/user/src/main/kotlin/kpring/user/entity/User.kt +++ b/user/src/main/kotlin/kpring/user/entity/User.kt @@ -15,6 +15,7 @@ class User( var email: String, @Column(nullable = false) var password: String, + var file: String?, @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "user_followers", @@ -39,9 +40,14 @@ class User( fun updateInfo( request: UpdateUserProfileRequest, newPassword: String?, + file: String?, ) { request.email?.let { this.email = it } request.username?.let { this.username = it } newPassword?.let { this.password = it } + + if (this.file != null || file != null) { + this.file = file + } } } diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index 3839ea52..13251677 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -41,6 +41,7 @@ class UserServiceImpl( multipartFile: MultipartFile, ): UpdateUserProfileResponse { var newPassword: String? = null + var uniqueFileName: String? = null val profileImgDir = Paths.get(System.getProperty("user.dir")).resolve(profileImgDirPath) val user = getUser(userId) @@ -51,9 +52,11 @@ class UserServiceImpl( } if (!multipartFile.isEmpty) { - saveUploadedFile(multipartFile, userId, profileImgDir) + uniqueFileName = saveUploadedFile(multipartFile, userId, profileImgDir) } - user.updateInfo(request, newPassword) + val previousFile = user.file + user.updateInfo(request, newPassword, uniqueFileName) + previousFile?.let { profileImgDir.resolve(it) }?.let { Files.deleteIfExists(it) } return UpdateUserProfileResponse(user.email, user.username) } @@ -73,6 +76,7 @@ class UserServiceImpl( email = request.email, password = password, username = request.username, + file = null, ), ) @@ -94,7 +98,7 @@ class UserServiceImpl( multipartFile: MultipartFile, userId: Long, dirPath: Path, - ) { + ): String { if (Files.notExists(dirPath)) { Files.createDirectories(dirPath) } @@ -104,6 +108,8 @@ class UserServiceImpl( val uniqueFileName = generateUniqueFileName(userId, extension) val filePath = dirPath.resolve(uniqueFileName) multipartFile.transferTo(filePath.toFile()) + + return uniqueFileName } private fun isFileExtensionSupported(extension: String) { From ea2fb3fb6d287e1b444edfa1d52f6f00627405ae Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 24 May 2024 15:50:42 +0900 Subject: [PATCH 010/178] =?UTF-8?q?refac:=20=EC=A7=80=EC=9B=90=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=99=95=EC=9E=A5=EC=9E=90=EC=9D=B8=EC=A7=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/service/UserServiceImpl.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index 13251677..34e8d74c 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -83,7 +83,7 @@ class UserServiceImpl( return CreateUserResponse(user.id, user.email) } - private fun handleDuplicateEmail(email: String) { + fun handleDuplicateEmail(email: String) { if (userRepository.existsByEmail(email)) { throw ServiceException(UserErrorCode.ALREADY_EXISTS_EMAIL) } @@ -103,7 +103,7 @@ class UserServiceImpl( Files.createDirectories(dirPath) } val extension = multipartFile.originalFilename!!.substringAfterLast('.') - isFileExtensionSupported(extension) + isFileExtensionSupported(multipartFile) val uniqueFileName = generateUniqueFileName(userId, extension) val filePath = dirPath.resolve(uniqueFileName) @@ -112,9 +112,9 @@ class UserServiceImpl( return uniqueFileName } - private fun isFileExtensionSupported(extension: String) { - val supportedExtensions = listOf("png", "jpg", "jpeg") - if (extension !in supportedExtensions) { + private fun isFileExtensionSupported(multipartFile: MultipartFile) { + val supportedExtensions = listOf("image/png", "image/jpeg") + if (multipartFile.contentType !in supportedExtensions) { throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) } } From c042c16eccd7a6df1f160727877ab974a378e79c Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 28 May 2024 16:33:36 +0900 Subject: [PATCH 011/178] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerTest.kt | 181 ++++++++++++++---- .../user/service/UserServiceImplTest.kt | 1 + .../resources/images/profileImg/testImg.jpg | Bin 0 -> 36220 bytes 3 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 user/src/test/resources/images/profileImg/testImg.jpg diff --git a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt index de1d9e6f..ea6a9971 100644 --- a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt @@ -24,13 +24,21 @@ import kpring.user.exception.UserErrorCode import kpring.user.service.UserService import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.core.io.ByteArrayResource +import org.springframework.core.io.ClassPathResource import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.http.client.MultipartBodyBuilder +import org.springframework.mock.web.MockMultipartFile import org.springframework.restdocs.ManualRestDocumentation import org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint +import org.springframework.restdocs.payload.PayloadDocumentation.* +import org.springframework.restdocs.request.RequestDocumentation.* import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext +import org.springframework.web.multipart.MultipartFile +import org.springframework.web.reactive.function.BodyInserters @WebMvcTest(controllers = [UserController::class]) @ExtendWith(value = [MockKExtension::class]) @@ -39,6 +47,7 @@ class UserControllerTest( webContext: WebApplicationContext, @MockkBean val authClient: AuthClient, @MockkBean val userService: UserService, + @MockkBean val multipartFile: MultipartFile, ) : DescribeSpec( { @@ -102,7 +111,6 @@ class UserControllerTest( } } } - it("회원가입 실패 : 이미 존재하는 이메일") { // given val request = @@ -147,7 +155,6 @@ class UserControllerTest( "username" type "String" mean "사용자 이름" } } - response { body { "message" type "String" mean "에러 메시지" @@ -155,7 +162,6 @@ class UserControllerTest( } } } - it("회원가입 실패 : 필수입력 값 미전송") { // given val request = @@ -198,7 +204,6 @@ class UserControllerTest( "username" type "String" mean "사용자 이름" } } - response { body { "message" type "String" mean "에러 메시지" @@ -206,7 +211,6 @@ class UserControllerTest( } } } - it("회원가입 실패 : 서버 내부 오류") { // given val request = @@ -251,7 +255,6 @@ class UserControllerTest( "username" type "String" mean "사용자 이름" } } - response { body { "message" type "String" mean "에러 메시지" @@ -260,26 +263,55 @@ class UserControllerTest( } } } - describe("회원정보 수정 API") { it("회원정보 수정 성공") { // given val userId = 1L - val request = UpdateUserProfileRequest.builder().email("test@test.com").build() - val data = UpdateUserProfileResponse.builder().email("test@test.com").build() + val request = + UpdateUserProfileRequest.builder() + .email(TEST_EMAIL) + .username(TEST_USERNAME) + .password(TEST_PASSWORD) + .newPassword(TEST_NEW_PASSWORD) + .build() + + val fileResource = ClassPathResource(TEST_PROFILE_IMG) + val file = + MockMultipartFile( + "image", + fileResource.filename, + MediaType.IMAGE_JPEG_VALUE, + fileResource.inputStream, + ) + val data = + UpdateUserProfileResponse.builder() + .email(TEST_EMAIL) + .username(TEST_USERNAME) + .build() + + val requestJson = objectMapper.writeValueAsString(request) + val response = ApiResponse(data = data) - every { userService.updateProfile(userId, request) } returns data every { authClient.getTokenInfo(any()) }.returns( ApiResponse(data = TokenInfo(TokenType.ACCESS, userId.toString())), ) + every { userService.updateProfile(userId, any(), any()) } returns data + + val bodyBuilder = MultipartBodyBuilder() + bodyBuilder.part("json", requestJson, MediaType.APPLICATION_JSON) + fileResource.filename?.let { + bodyBuilder.part("file", ByteArrayResource(file.bytes), MediaType.IMAGE_JPEG).filename( + it, + ) + } // when val result = webTestClient.patch() .uri("/api/v1/user/{userId}", userId) .header("Authorization", "Bearer token") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(request) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(bodyBuilder.build())) .exchange() // then @@ -296,15 +328,27 @@ class UserControllerTest( ) { request { header { - "Content-Type" mean "application/json" + "Authorization" mean "Bearer token" + "Content-Type" mean "multipart/form-data" } - body { - "email" type "String" mean "이메일" + path { + "userId" mean "사용자 아이디" + } + part { + "json" mean "회원정보 수정 요청 JSON" + "file" mean "프로필 이미지 파일" + } + part("json") { + "email" mean "이메일" + "username" mean "닉네임" + "password" mean "기존 비밀번호" + "newPassword" mean "새 비밀번호" } } response { body { "data.email" type "String" mean "이메일" + "data.username" type "String" mean "닉네임" } } } @@ -313,18 +357,45 @@ class UserControllerTest( it("회원정보 수정 실패 : 권한이 없는 토큰") { // given val userId = 1L - val request = UpdateUserProfileRequest.builder().email("test@test.com").build() + val request = + UpdateUserProfileRequest.builder() + .email(TEST_EMAIL) + .username(TEST_USERNAME) + .password(TEST_PASSWORD) + .newPassword(TEST_NEW_PASSWORD) + .build() + + val fileResource = ClassPathResource(TEST_PROFILE_IMG) + val file = + MockMultipartFile( + "image", + fileResource.filename, + MediaType.IMAGE_JPEG_VALUE, + fileResource.inputStream, + ) + + val requestJson = objectMapper.writeValueAsString(request) + val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + val bodyBuilder = + MultipartBodyBuilder() + bodyBuilder.part("json", requestJson, MediaType.APPLICATION_JSON) + fileResource.filename?.let { + bodyBuilder.part("file", ByteArrayResource(file.bytes), MediaType.IMAGE_JPEG).filename( + it, + ) + } + // when val result = webTestClient.patch() .uri("/api/v1/user/{userId}", userId) .header("Authorization", "Bearer token") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(request) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(bodyBuilder.build())) .exchange() // then @@ -341,10 +412,21 @@ class UserControllerTest( ) { request { header { - "Content-Type" mean "application/json" + "Content-Type" mean "multipart/form-data" + "Authorization" mean "Bearer token" } - body { - "email" type "String" mean "이메일" + path { + "userId" mean "사용자 아이디" + } + part { + "json" mean "회원정보 수정 요청 JSON" + "file" mean "프로필 이미지 파일" + } + part("json") { + "email" mean "이메일" + "username" mean "닉네임" + "password" mean "기존 비밀번호" + "newPassword" mean "새 비밀번호" } } response { @@ -354,22 +436,47 @@ class UserControllerTest( } } } - it("회원정보 수정 실패 : 서버 내부 오류") { // given val userId = 1L val token = "Bearer token" - val request = UpdateUserProfileRequest.builder().email("test@test.com").build() + val request = + UpdateUserProfileRequest.builder() + .email(TEST_EMAIL) + .username(TEST_USERNAME) + .password(TEST_PASSWORD) + .newPassword(TEST_NEW_PASSWORD) + .build() + + val fileResource = ClassPathResource(TEST_PROFILE_IMG) + val file = + MockMultipartFile( + "image", + fileResource.filename, + MediaType.IMAGE_JPEG_VALUE, + fileResource.inputStream, + ) + val requestJson = objectMapper.writeValueAsString(request) + val response = FailMessageResponse.serverError every { authClient.getTokenInfo(token) } throws RuntimeException("서버 내부 오류") + val bodyBuilder = + MultipartBodyBuilder() + bodyBuilder.part("json", requestJson, MediaType.APPLICATION_JSON) + fileResource.filename?.let { + bodyBuilder.part("file", ByteArrayResource(file.bytes), MediaType.IMAGE_JPEG).filename( + it, + ) + } + // when val result = webTestClient.patch() .uri("/api/v1/user/{userId}", userId) .header("Authorization", "Bearer token") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(request) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(bodyBuilder.build())) .exchange() // then @@ -386,10 +493,20 @@ class UserControllerTest( ) { request { header { - "Content-Type" mean "application/json" + "Content-Type" mean "multipart/form-data" } - body { - "email" type "String" mean "이메일" + path { + "userId" mean "사용자 아이디" + } + part { + "json" mean "회원정보 수정 요청 JSON" + "file" mean "프로필 이미지 파일" + } + part("json") { + "email" mean "이메일" + "username" mean "닉네임" + "password" mean "기존 비밀번호" + "newPassword" mean "새 비밀번호" } } response { @@ -400,7 +517,6 @@ class UserControllerTest( } } } - describe("프로필 조회 API") { it("조회 성공") { // given @@ -454,7 +570,6 @@ class UserControllerTest( } } } - it("조회 실패 : 권한이 없는 토큰") { // given val userId = 1L @@ -491,7 +606,6 @@ class UserControllerTest( } } } - it("조회 실패 : 서버 내부 오류") { // given val userId = 1L @@ -526,7 +640,6 @@ class UserControllerTest( } } } - describe("탈퇴 API") { it("탈퇴 성공") { // given @@ -564,7 +677,6 @@ class UserControllerTest( } } } - it("탈퇴 실패 : 권한이 없는 토큰") { // given val userId = 1L @@ -597,7 +709,6 @@ class UserControllerTest( } } } - it("탈퇴 실패 : 서버 내부 오류") { // given val userId = 1L @@ -639,6 +750,8 @@ class UserControllerTest( companion object { private const val TEST_EMAIL = "test@email.com" private const val TEST_PASSWORD = "tesT@1234" + private const val TEST_NEW_PASSWORD = "tesT@1234!" private const val TEST_USERNAME = "testuser" + private const val TEST_PROFILE_IMG = "/images/profileImg" } } diff --git a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt index 96658129..f7b728c4 100644 --- a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt @@ -43,6 +43,7 @@ class UserServiceImplTest : FunSpec({ createUserRequest.username, createUserRequest.email, createUserRequest.password, + null, ) every { userService.handleDuplicateEmail(createUserRequest.email) } just Runs diff --git a/user/src/test/resources/images/profileImg/testImg.jpg b/user/src/test/resources/images/profileImg/testImg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d4e6dee3d62f300043e78ee9f37a33ea05e28f4 GIT binary patch literal 36220 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 p3=gOQQ`kI#nkf-v)?;RN1?C Date: Tue, 28 May 2024 16:51:12 +0900 Subject: [PATCH 012/178] =?UTF-8?q?refac:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UploadProfileImageService.kt | 46 +++++++++++++++++++ .../kpring/user/service/UserServiceImpl.kt | 40 ++-------------- .../user/service/UserServiceImplTest.kt | 2 + 3 files changed, 51 insertions(+), 37 deletions(-) create mode 100644 user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt diff --git a/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt b/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt new file mode 100644 index 00000000..afc45a29 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt @@ -0,0 +1,46 @@ +package kpring.user.service + +import kpring.core.global.exception.ServiceException +import kpring.user.exception.UserErrorCode +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile +import java.nio.file.Files +import java.nio.file.Path +import java.text.SimpleDateFormat +import java.util.* + +@Service +class UploadProfileImageService { + public fun saveUploadedFile( + multipartFile: MultipartFile, + userId: Long, + dirPath: Path, + ): String { + if (Files.notExists(dirPath)) { + Files.createDirectories(dirPath) + } + val extension = multipartFile.originalFilename!!.substringAfterLast('.') + isFileExtensionSupported(multipartFile) + + val uniqueFileName = generateUniqueFileName(userId, extension) + val filePath = dirPath.resolve(uniqueFileName) + multipartFile.transferTo(filePath.toFile()) + + return uniqueFileName + } + + private fun isFileExtensionSupported(multipartFile: MultipartFile) { + val supportedExtensions = listOf("image/png", "image/jpeg") + if (multipartFile.contentType !in supportedExtensions) { + throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) + } + } + + private fun generateUniqueFileName( + userId: Long, + extension: String, + ): String { + val timeStamp = SimpleDateFormat("yyMMddHHmmss").format(Date()) + return "$timeStamp$userId.$extension" + } +} diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index 34e8d74c..7e4d0fa9 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -15,10 +15,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths -import java.text.SimpleDateFormat -import java.util.* @Service @Transactional @@ -26,6 +23,7 @@ class UserServiceImpl( private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder, private val userValidationService: UserValidationService, + private val uploadProfileImageService: UploadProfileImageService, ) : UserService { @Value("\${file.path.profile-dir}") private lateinit var profileImgDirPath: String @@ -52,7 +50,8 @@ class UserServiceImpl( } if (!multipartFile.isEmpty) { - uniqueFileName = saveUploadedFile(multipartFile, userId, profileImgDir) + uniqueFileName = + uploadProfileImageService.saveUploadedFile(multipartFile, userId, profileImgDir) } val previousFile = user.file user.updateInfo(request, newPassword, uniqueFileName) @@ -93,37 +92,4 @@ class UserServiceImpl( return userRepository.findById(userId) .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } } - - private fun saveUploadedFile( - multipartFile: MultipartFile, - userId: Long, - dirPath: Path, - ): String { - if (Files.notExists(dirPath)) { - Files.createDirectories(dirPath) - } - val extension = multipartFile.originalFilename!!.substringAfterLast('.') - isFileExtensionSupported(multipartFile) - - val uniqueFileName = generateUniqueFileName(userId, extension) - val filePath = dirPath.resolve(uniqueFileName) - multipartFile.transferTo(filePath.toFile()) - - return uniqueFileName - } - - private fun isFileExtensionSupported(multipartFile: MultipartFile) { - val supportedExtensions = listOf("image/png", "image/jpeg") - if (multipartFile.contentType !in supportedExtensions) { - throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) - } - } - - private fun generateUniqueFileName( - userId: Long, - extension: String, - ): String { - val timeStamp = SimpleDateFormat("yyMMddHHmmss").format(Date()) - return "$timeStamp$userId.$extension" - } } diff --git a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt index f7b728c4..0aa7ddd4 100644 --- a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt @@ -16,11 +16,13 @@ class UserServiceImplTest : FunSpec({ val userRepository: UserRepository = mockk() val passwordEncoder: PasswordEncoder = mockk() val userValidationService: UserValidationService = mockk() + val uploadProfileImageService: UploadProfileImageService = mockk() val userService = UserServiceImpl( userRepository, passwordEncoder, userValidationService, + uploadProfileImageService, ) val friendService = FriendService(userRepository) lateinit var createUserRequest: CreateUserRequest From 50fbd8b2aec504172c53dacd8411082215167905 Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 29 May 2024 15:48:12 +0900 Subject: [PATCH 013/178] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/dto/request/UpdateUserProfileRequest.java | 11 ++++++++++- .../kotlin/kpring/user/controller/UserController.kt | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java b/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java index 4c483e77..e8a5a692 100644 --- a/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java +++ b/user/src/main/java/kpring/user/dto/request/UpdateUserProfileRequest.java @@ -1,13 +1,22 @@ package kpring.user.dto.request; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; import lombok.Builder; -import org.springframework.web.multipart.MultipartFile; @Builder public record UpdateUserProfileRequest( + + @Email(message = "invalid email") String email, + + @Pattern(regexp = "^[a-zA-Z0-9가-힣]{1,32}$", + message = "닉네임은 영문 대소문자, 숫자, 한글로 구성되어야 하며, 1자 이상 32자 이하여야 합니다.") String username, String password, + @Pattern(regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])[a-zA-Z0-9!@#$]{8,15}$", + message = "비밀번호는 최소 8자에서 15자 사이, 대문자와 소문자, 숫자가 포함되어야 하며, " + + "특수문자 (!, @, #, $)도 사용할 수 있습니다.") String newPassword ) { diff --git a/user/src/main/kotlin/kpring/user/controller/UserController.kt b/user/src/main/kotlin/kpring/user/controller/UserController.kt index ba18bc86..8c4c78a2 100644 --- a/user/src/main/kotlin/kpring/user/controller/UserController.kt +++ b/user/src/main/kotlin/kpring/user/controller/UserController.kt @@ -44,7 +44,7 @@ class UserController( fun updateUserProfile( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, - @RequestPart(value = "json") request: UpdateUserProfileRequest, + @Validated @RequestPart(value = "json") request: UpdateUserProfileRequest, @RequestPart(value = "file") multipartFile: MultipartFile, ): ResponseEntity> { val validatedUserId = checkIfAccessTokenAndGetUserId(token) From 7d7cc9b99184116b2593b8adc498a125d0cfd5e5 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:02:40 +0900 Subject: [PATCH 014/178] =?UTF-8?q?chore:=20db=20setting=EA=B3=BC=20proper?= =?UTF-8?q?ties=20application.yml=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/resources/application.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/chat/src/main/resources/application.yml b/chat/src/main/resources/application.yml index 0590f9e8..8c912833 100644 --- a/chat/src/main/resources/application.yml +++ b/chat/src/main/resources/application.yml @@ -11,12 +11,21 @@ spring: authentication-database: admin authSource: admin + redis: + host: localhost + port: 6379 + password: testpassword1234 + server: port: 8081 auth: - url: "http://localhost:30000/" + url: "http://localhost:30001/" url: - server: "http://localhost:8080/" + server: "http://localhost:8080/" + page: size: 100 +chatroom: + expiration: 1d + baseurl: "http://localhost:8081/" From 0f52c3d7405097c286838e226deee51b9703f4f0 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:02:55 +0900 Subject: [PATCH 015/178] =?UTF-8?q?chore:=20build.gradle=EC=97=90=20redis?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat/build.gradle.kts b/chat/build.gradle.kts index 019abc56..6f1757a4 100644 --- a/chat/build.gradle.kts +++ b/chat/build.gradle.kts @@ -41,6 +41,9 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + // non-blocking redis + implementation("org.springframework.boot:spring-boot-starter-data-redis") + // test testImplementation(project(":test")) testImplementation("org.springframework.boot:spring-boot-starter-test") From 9a93d030e793d7d4f677d2f967897cd04b7863df Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:03:38 +0900 Subject: [PATCH 016/178] =?UTF-8?q?feat:=20getChatRoomInvitation=20Control?= =?UTF-8?q?ler=20Layer=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/api/v1/ChatRoomController.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt index 85829ec3..386e5702 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt @@ -3,6 +3,7 @@ package kpring.chat.chatroom.api.v1 import kpring.chat.chatroom.service.ChatRoomService import kpring.core.auth.client.AuthClient import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest +import kpring.core.global.dto.response.ApiResponse import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* @@ -34,4 +35,14 @@ class ChatRoomController( val result = chatRoomService.exitChatRoom(chatRoomId, userId) return ResponseEntity.ok().body(result) } + + @GetMapping("/chatroom/{chatRoomId}/invite") + fun getChatRoomInvitation( + @PathVariable("chatRoomId") chatRoomId: String, + @RequestHeader("Authorization") token: String, + ): ResponseEntity<*> { + val userId = authClient.getTokenInfo(token).data!!.userId + val result = chatRoomService.getChatRoomInvitation(chatRoomId, userId) + return ResponseEntity.ok().body(ApiResponse(data = result)) + } } From edd40edcd30ab61ebdce0ad272081908b759cf17 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:04:23 +0900 Subject: [PATCH 017/178] =?UTF-8?q?feat:=20getChatRoomInvitation=20Service?= =?UTF-8?q?=20Layer=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/service/ChatRoomService.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index f277d55f..3ec62c80 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -2,14 +2,17 @@ package kpring.chat.chatroom.service import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository +import kpring.chat.chatroom.repository.UserChatRoomInvitationRepository import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException +import kpring.core.chat.chat.dto.response.InvitationResponse import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest import org.springframework.stereotype.Service @Service class ChatRoomService( private val chatRoomRepository: ChatRoomRepository, + private val userChatRoomInvitationRepository: UserChatRoomInvitationRepository, ) { fun createChatRoom( request: CreateChatRoomRequest, @@ -30,6 +33,14 @@ class ChatRoomService( chatRoomRepository.save(chatRoom) } + fun getChatRoomInvitation( + chatRoomId: String, + userId: String, + ): InvitationResponse { + verifyChatRoomAccess(chatRoomId, userId) + return userChatRoomInvitationRepository.getInvitation(chatRoomId, userId) + } + fun verifyChatRoomAccess( chatRoomId: String, userId: String, From 66ffbc682cedccd4e4cfce819237294647bf801a Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:04:36 +0900 Subject: [PATCH 018/178] =?UTF-8?q?feat:=20ErrorCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt index 09798d7a..d3e2f5fb 100644 --- a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt +++ b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt @@ -12,4 +12,8 @@ enum class ErrorCode(val httpStatus: Int, val message: String) { // 404 CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"), + + // 500 + INVITATION_LINK_SAVE_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invitation Link가 저장되지 않았습니다"), + GET_EXPIRATION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Key의 expiration time을 가져올 수 없습니다"), } From b0b114c0fccb72c13ccb8941ec872c13cc5d864b Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:07:02 +0900 Subject: [PATCH 019/178] =?UTF-8?q?feat:=20UserChatRoomInvitationRepositor?= =?UTF-8?q?y=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserChatRoomInvitationRepository.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt new file mode 100644 index 00000000..fbd67bdf --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt @@ -0,0 +1,19 @@ +package kpring.chat.chatroom.repository + +import kpring.chat.global.config.PropertyConfig +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException +import kpring.core.chat.chat.dto.response.InvitationResponse +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ValueOperations +import org.springframework.stereotype.Component +import java.time.LocalDateTime +import java.util.* + +@Component +class UserChatRoomInvitationRepository( + private val redisTemplate: RedisTemplate, + private val propertyConfig: PropertyConfig, + private val invitationChatRoomRepository: InvitationChatRoomRepository, +) { +} From 5b70b4b92d8d91a5e32b1d0123aa87651e8bdb02 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:08:13 +0900 Subject: [PATCH 020/178] =?UTF-8?q?feat:=20UserChatRoomInvitationRepositor?= =?UTF-8?q?y=EC=97=90=20setInvitation,=20getInvitation=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserChatRoomInvitationRepository.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt index fbd67bdf..5bd51b7a 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt @@ -16,4 +16,48 @@ class UserChatRoomInvitationRepository( private val propertyConfig: PropertyConfig, private val invitationChatRoomRepository: InvitationChatRoomRepository, ) { + fun getInvitation( + userId: String, + chatRoomId: String, + ): InvitationResponse { + val key = generateKey(userId, chatRoomId) + var value = redisTemplate.opsForValue().get(key) + if (value == null) { + value = setInvitation(key, chatRoomId) + } + + val expiration = getExpiration(key) + + return InvitationResponse(value, LocalDateTime.now().plusSeconds(expiration).toString()) + } + + fun setInvitation( + key: String, + chatRoomId: String, + ): String { + val invitationLink = generateLink() + val ops: ValueOperations = redisTemplate.opsForValue() + ops.set(key, invitationLink, propertyConfig.getExpiration()) + invitationChatRoomRepository.setInvitationLink(invitationLink, chatRoomId) + return invitationLink + } + + private fun generateKey( + userId: String, + chatRoomId: String, + ): String { + return "$userId:$chatRoomId" + } + + private fun generateLink(): String { + return UUID.randomUUID().toString() + } + + private fun getExpiration(key: String): Long { + val expiration = redisTemplate.getExpire(key) + if (expiration == null || expiration == -1L) { + throw GlobalException(ErrorCode.GET_EXPIRATION_FAILURE) + } + return expiration + } } From 9826a242a92ffa70f3b66e8d14a61c20ba0f596b Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:09:06 +0900 Subject: [PATCH 021/178] =?UTF-8?q?feat:=20InvitationChatRoomRepository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/InvitationChatRoomRepository.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt new file mode 100644 index 00000000..f7fb302c --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt @@ -0,0 +1,11 @@ +package kpring.chat.chatroom.repository + +import kpring.chat.global.config.PropertyConfig +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ValueOperations +import org.springframework.stereotype.Component + +@Component +class InvitationChatRoomRepository From 0a934bad4395879771b1368b83afd27573793e85 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:10:00 +0900 Subject: [PATCH 022/178] =?UTF-8?q?feat:=20InvitationChatRoomRepository?= =?UTF-8?q?=EC=97=90=20setInvitationLink=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/InvitationChatRoomRepository.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt index f7fb302c..5e514933 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt @@ -8,4 +8,19 @@ import org.springframework.data.redis.core.ValueOperations import org.springframework.stereotype.Component @Component -class InvitationChatRoomRepository +class InvitationChatRoomRepository( + private val redisTemplate: RedisTemplate, + private val propertyConfig: PropertyConfig, +) { + fun setInvitationLink( + invitationLink: String, + chatRoomId: String, + ) { + val ops: ValueOperations = redisTemplate.opsForValue() + val result = ops.setIfAbsent(invitationLink, chatRoomId, propertyConfig.getExpiration()) + + if (result == null || !result) { + throw GlobalException(ErrorCode.INVITATION_LINK_SAVE_FAILURE) + } + } +} From 7dfae340b33627ed3bc7306a554d6fc4815bf085 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:10:31 +0900 Subject: [PATCH 023/178] =?UTF-8?q?feat:=20InvitationResponse=20DTO=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/chat/chat/dto/response/InvitationResponse.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt diff --git a/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt new file mode 100644 index 00000000..df0a2a56 --- /dev/null +++ b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt @@ -0,0 +1,6 @@ +package kpring.core.chat.chat.dto.response + +data class InvitationResponse( + val key: String, + val expiration: String, +) From a5a16e15580cb94e8df086a1681306c28b93015e Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:10:48 +0900 Subject: [PATCH 024/178] =?UTF-8?q?feat:=20PropertyConfig=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/global/config/PropertyConfig.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt diff --git a/chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt b/chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt new file mode 100644 index 00000000..a723b30e --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt @@ -0,0 +1,19 @@ +package kpring.chat.global.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration +import java.time.Duration + +@Configuration +@ConfigurationProperties(prefix = "chatroom") +class PropertyConfig { + private lateinit var expiration: Duration + + fun getExpiration(): Duration { + return expiration + } + + fun setExpiration(expiration: Duration) { + this.expiration = expiration + } +} From d67ff434b049e178043648318be1b3a4c8d01f88 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:10:57 +0900 Subject: [PATCH 025/178] =?UTF-8?q?feat:=20RedisConfig=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/global/config/RedisConfig.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/global/config/RedisConfig.kt diff --git a/chat/src/main/kotlin/kpring/chat/global/config/RedisConfig.kt b/chat/src/main/kotlin/kpring/chat/global/config/RedisConfig.kt new file mode 100644 index 00000000..07a6e51e --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/global/config/RedisConfig.kt @@ -0,0 +1,16 @@ +package kpring.chat.global.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate + +@Configuration +class RedisConfig { + @Bean + fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<*, *> { + val template = RedisTemplate() + template.connectionFactory = connectionFactory + return template + } +} From 18b7f84a7d2ea15247e0f5378566ad21054adc63 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:11:22 +0900 Subject: [PATCH 026/178] =?UTF-8?q?chore:=20Redis=20docker=20image=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/compose.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/chat/compose.yml b/chat/compose.yml index b7c7023d..66c48662 100644 --- a/chat/compose.yml +++ b/chat/compose.yml @@ -3,8 +3,17 @@ services: image: mongo:latest container_name: mongo ports: - - "27017:27017" + - "27018:27017" environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: 58155815 - MONGO_INITDB_DATABASE: mongodb \ No newline at end of file + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: testpassword1234 + MONGO_INITDB_DATABASE: mongodb + + redis: + image: redis:alpine + container_name: redis_link + ports: + - "6379:6379" + environment: + - REDIS_PASSWORD = "testpassword1234" + command: [ "redis-server","--requirepass","${REDIS_PASSWORD}" ] \ No newline at end of file From 47038d4ca6d83c7597650fc2d2cfa2d9e0464032 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:12:10 +0900 Subject: [PATCH 027/178] =?UTF-8?q?feat:=20ChatRoomControllerTest=EC=97=90?= =?UTF-8?q?=20getChatRoomInvitation=20api=20testcode=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/api/v1/ChatRoomControllerTest.kt | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt new file mode 100644 index 00000000..f3b45e86 --- /dev/null +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -0,0 +1,103 @@ +package kpring.chat.chat.api.v1 + +import com.fasterxml.jackson.databind.ObjectMapper +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import io.mockk.junit5.MockKExtension +import kpring.chat.chatroom.api.v1.ChatRoomController +import kpring.chat.chatroom.service.ChatRoomService +import kpring.chat.global.ChatRoomTest +import kpring.chat.global.CommonTest +import kpring.core.auth.client.AuthClient +import kpring.core.auth.dto.response.TokenInfo +import kpring.core.auth.enums.TokenType +import kpring.core.chat.chat.dto.response.InvitationResponse +import kpring.core.global.dto.response.ApiResponse +import kpring.test.restdoc.dsl.restDoc +import kpring.test.restdoc.json.JsonDataType +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.RestDocumentationExtension +import org.springframework.restdocs.operation.preprocess.Preprocessors +import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.MockMvcWebTestClient +import org.springframework.web.context.WebApplicationContext +import java.time.LocalDateTime + +@WebMvcTest(controllers = [ChatRoomController::class]) +@ExtendWith(RestDocumentationExtension::class) +@ExtendWith(SpringExtension::class) +@ExtendWith(MockKExtension::class) +class ChatRoomControllerTest( + private val om: ObjectMapper, + webContext: WebApplicationContext, + @MockkBean val chatRoomService: ChatRoomService, + @MockkBean val authClient: AuthClient, +) : DescribeSpec({ + + val restDocument = ManualRestDocumentation() + val webTestClient: WebTestClient = + MockMvcWebTestClient.bindToApplicationContext(webContext).configureClient().baseUrl("http://localhost:8081").filter( + WebTestClientRestDocumentation.documentationConfiguration(restDocument).operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()).withResponseDefaults(Preprocessors.prettyPrint()), + ).build() + + beforeSpec { restDocument.beforeTest(this.javaClass, "chat controller") } + + afterSpec { restDocument.afterTest() } + + describe("GET /api/v1/chatroom/{chatRoomId}/invite : getChatRoomInvitation api test") { + + val url = "/api/v1/chatroom/{chatRoomId}/invite" + it("getChatRoomInvitation api test") { + + // Given + val chatRoomId = ChatRoomTest.TEST_ROOM_ID + val userId = CommonTest.TEST_USER_ID + val key = "62e9df6b-13cb-4673-a6fe-8566451b7f15" + val expiration = LocalDateTime.now().plusSeconds(3600).toString() + val data = InvitationResponse(key, expiration) + + every { authClient.getTokenInfo(any()) } returns ApiResponse( + data = TokenInfo( + type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID, + ), + ) + + every { + chatRoomService.getChatRoomInvitation( + chatRoomId, + userId, + ) + } returns data + + // When + val result = webTestClient.get().uri(url, chatRoomId).header("Authorization", "Bearer mock_token").exchange() + + val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(data = data))) + + // Then + docs.restDoc( + identifier = "getChatRoomInvitation_200", + description = "채팅방 참여코드를 위한 key값을 반환하는 api", + ) { + request { + path { + "chatRoomId" mean "채팅방 참여코드를 발급할 채팅방 Id" + } + } + + response { + body { + "data.key" type JsonDataType.Strings mean "참여 코드" + "data.expiration" type JsonDataType.Strings mean "참여코드 유효시간" + } + } + } + } + } +}) From a9c1453d2575ddf6061d64c275c08c4f630802ad Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:12:43 +0900 Subject: [PATCH 028/178] =?UTF-8?q?refac:=20=EC=B6=94=EA=B0=80=EB=90=9C=20?= =?UTF-8?q?repository=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt index 49d0ba99..55207914 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt @@ -7,6 +7,7 @@ import io.mockk.mockk import io.mockk.verify import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository +import kpring.chat.chatroom.repository.UserChatRoomInvitationRepository import kpring.chat.chatroom.service.ChatRoomService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest @@ -16,7 +17,8 @@ import java.util.* class ChatRoomServiceTest : FunSpec({ val chatRoomRepository = mockk() - val chatRoomService = ChatRoomService(chatRoomRepository) + val userChatRoomInvitationRepository = mockk() + val chatRoomService = ChatRoomService(chatRoomRepository, userChatRoomInvitationRepository) test("createChatRoom 는 새 ChatRoom을 저장해야 한다") { // Given From 406d11dde1c0fdf9dc191b1acb8b7fd5f233caf9 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:18:59 +0900 Subject: [PATCH 029/178] =?UTF-8?q?refac:=20=EB=A9=94=EC=84=9C=EB=93=9C,?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=9D=B4=EB=A6=84=20link,key=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20code=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvitationChatRoomRepository.kt | 2 +- .../chatroom/api/v1/ChatRoomControllerTest.kt | 94 ++++++++++--------- .../chat/dto/response/InvitationResponse.kt | 2 +- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt index 5e514933..2b44e3fc 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt @@ -12,7 +12,7 @@ class InvitationChatRoomRepository( private val redisTemplate: RedisTemplate, private val propertyConfig: PropertyConfig, ) { - fun setInvitationLink( + fun setInvitationCode( invitationLink: String, chatRoomId: String, ) { diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt index f3b45e86..d374cd1d 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -39,65 +39,67 @@ class ChatRoomControllerTest( @MockkBean val authClient: AuthClient, ) : DescribeSpec({ - val restDocument = ManualRestDocumentation() - val webTestClient: WebTestClient = - MockMvcWebTestClient.bindToApplicationContext(webContext).configureClient().baseUrl("http://localhost:8081").filter( - WebTestClientRestDocumentation.documentationConfiguration(restDocument).operationPreprocessors() - .withRequestDefaults(Preprocessors.prettyPrint()).withResponseDefaults(Preprocessors.prettyPrint()), - ).build() + val restDocument = ManualRestDocumentation() + val webTestClient: WebTestClient = + MockMvcWebTestClient.bindToApplicationContext(webContext).configureClient().baseUrl("http://localhost:8081").filter( + WebTestClientRestDocumentation.documentationConfiguration(restDocument).operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()).withResponseDefaults(Preprocessors.prettyPrint()), + ).build() - beforeSpec { restDocument.beforeTest(this.javaClass, "chat controller") } + beforeSpec { restDocument.beforeTest(this.javaClass, "chat controller") } - afterSpec { restDocument.afterTest() } + afterSpec { restDocument.afterTest() } - describe("GET /api/v1/chatroom/{chatRoomId}/invite : getChatRoomInvitation api test") { + describe("GET /api/v1/chatroom/{chatRoomId}/invite : getChatRoomInvitation api test") { - val url = "/api/v1/chatroom/{chatRoomId}/invite" - it("getChatRoomInvitation api test") { + val url = "/api/v1/chatroom/{chatRoomId}/invite" + it("getChatRoomInvitation api test") { - // Given - val chatRoomId = ChatRoomTest.TEST_ROOM_ID - val userId = CommonTest.TEST_USER_ID - val key = "62e9df6b-13cb-4673-a6fe-8566451b7f15" - val expiration = LocalDateTime.now().plusSeconds(3600).toString() - val data = InvitationResponse(key, expiration) + // Given + val chatRoomId = ChatRoomTest.TEST_ROOM_ID + val userId = CommonTest.TEST_USER_ID + val key = "62e9df6b-13cb-4673-a6fe-8566451b7f15" + val expiration = LocalDateTime.now().plusSeconds(3600).toString() + val data = InvitationResponse(key, expiration) - every { authClient.getTokenInfo(any()) } returns ApiResponse( - data = TokenInfo( - type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID, - ), - ) + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID, + ), + ) - every { - chatRoomService.getChatRoomInvitation( - chatRoomId, - userId, - ) - } returns data + every { + chatRoomService.getChatRoomInvitation( + chatRoomId, + userId, + ) + } returns data - // When - val result = webTestClient.get().uri(url, chatRoomId).header("Authorization", "Bearer mock_token").exchange() + // When + val result = webTestClient.get().uri(url, chatRoomId).header("Authorization", "Bearer mock_token").exchange() - val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(data = data))) + val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(data = data))) - // Then - docs.restDoc( - identifier = "getChatRoomInvitation_200", - description = "채팅방 참여코드를 위한 key값을 반환하는 api", - ) { - request { - path { - "chatRoomId" mean "채팅방 참여코드를 발급할 채팅방 Id" + // Then + docs.restDoc( + identifier = "getChatRoomInvitation_200", + description = "채팅방 참여코드를 위한 key값을 반환하는 api", + ) { + request { + path { + "chatRoomId" mean "채팅방 참여코드를 발급할 채팅방 Id" + } } - } - response { - body { - "data.key" type JsonDataType.Strings mean "참여 코드" - "data.expiration" type JsonDataType.Strings mean "참여코드 유효시간" + response { + body { + "data.code" type JsonDataType.Strings mean "참여 코드" + "data.expiration" type JsonDataType.Strings mean "참여코드 유효시간" + } } } } } - } -}) + }) diff --git a/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt index df0a2a56..fafc7c4a 100644 --- a/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt +++ b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt @@ -1,6 +1,6 @@ package kpring.core.chat.chat.dto.response data class InvitationResponse( - val key: String, + val code: String, val expiration: String, ) From a7973469df084ffc3adf809b02cebbbcf555836d Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:20:40 +0900 Subject: [PATCH 030/178] =?UTF-8?q?refac:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserChatRoomInvitationRepository.kt | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt index 5bd51b7a..c5b49bd0 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt @@ -26,7 +26,7 @@ class UserChatRoomInvitationRepository( value = setInvitation(key, chatRoomId) } - val expiration = getExpiration(key) + val expiration = redisTemplate.getExpire(key) return InvitationResponse(value, LocalDateTime.now().plusSeconds(expiration).toString()) } @@ -35,11 +35,11 @@ class UserChatRoomInvitationRepository( key: String, chatRoomId: String, ): String { - val invitationLink = generateLink() + val invitationCode = generateCode() val ops: ValueOperations = redisTemplate.opsForValue() - ops.set(key, invitationLink, propertyConfig.getExpiration()) - invitationChatRoomRepository.setInvitationLink(invitationLink, chatRoomId) - return invitationLink + ops.set(key, invitationCode, propertyConfig.getExpiration()) + invitationChatRoomRepository.setInvitationCode(invitationCode, chatRoomId) + return invitationCode } private fun generateKey( @@ -49,15 +49,7 @@ class UserChatRoomInvitationRepository( return "$userId:$chatRoomId" } - private fun generateLink(): String { + private fun generateCode(): String { return UUID.randomUUID().toString() } - - private fun getExpiration(key: String): Long { - val expiration = redisTemplate.getExpire(key) - if (expiration == null || expiration == -1L) { - throw GlobalException(ErrorCode.GET_EXPIRATION_FAILURE) - } - return expiration - } } From 7ecfec9ec49d1f3da5b8ff44850d22eccaf8030f Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 02:21:27 +0900 Subject: [PATCH 031/178] =?UTF-8?q?refac:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20ErrorCode=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt index d3e2f5fb..2d5c4779 100644 --- a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt +++ b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt @@ -14,6 +14,5 @@ enum class ErrorCode(val httpStatus: Int, val message: String) { CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"), // 500 - INVITATION_LINK_SAVE_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invitation Link가 저장되지 않았습니다"), - GET_EXPIRATION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Key의 expiration time을 가져올 수 없습니다"), + INVITATION_LINK_SAVE_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invitation Code가 저장되지 않았습니다"), } From 46e80af1d6851edec137b9891cf6af69e4323a82 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 4 Jun 2024 22:53:48 +0900 Subject: [PATCH 032/178] =?UTF-8?q?refac:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20repository=EC=97=90=EC=84=9C=20service?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B8=B0=EA=B3=A0=20return=20type?= =?UTF-8?q?=EC=9D=B4=20ResponseDTO=EA=B0=80=20=EC=95=84=EB=8B=88=EA=B2=8C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserChatRoomInvitationRepository.kt | 21 ++++++++++--------- .../chat/chatroom/service/ChatRoomService.kt | 5 ++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt index c5b49bd0..8d1d345d 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt @@ -1,13 +1,9 @@ package kpring.chat.chatroom.repository import kpring.chat.global.config.PropertyConfig -import kpring.chat.global.exception.ErrorCode -import kpring.chat.global.exception.GlobalException -import kpring.core.chat.chat.dto.response.InvitationResponse import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.ValueOperations import org.springframework.stereotype.Component -import java.time.LocalDateTime import java.util.* @Component @@ -16,19 +12,16 @@ class UserChatRoomInvitationRepository( private val propertyConfig: PropertyConfig, private val invitationChatRoomRepository: InvitationChatRoomRepository, ) { - fun getInvitation( + fun getInvitationCode( userId: String, chatRoomId: String, - ): InvitationResponse { + ): String { val key = generateKey(userId, chatRoomId) var value = redisTemplate.opsForValue().get(key) if (value == null) { value = setInvitation(key, chatRoomId) } - - val expiration = redisTemplate.getExpire(key) - - return InvitationResponse(value, LocalDateTime.now().plusSeconds(expiration).toString()) + return value } fun setInvitation( @@ -42,6 +35,14 @@ class UserChatRoomInvitationRepository( return invitationCode } + fun getExpiration( + userId: String, + chatRoomId: String, + ): Long{ + val key = chatRoomId + return redisTemplate.getExpire(key) + } + private fun generateKey( userId: String, chatRoomId: String, diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index 3ec62c80..f00b9e05 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -8,6 +8,7 @@ import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.response.InvitationResponse import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest import org.springframework.stereotype.Service +import java.time.LocalDateTime @Service class ChatRoomService( @@ -38,7 +39,9 @@ class ChatRoomService( userId: String, ): InvitationResponse { verifyChatRoomAccess(chatRoomId, userId) - return userChatRoomInvitationRepository.getInvitation(chatRoomId, userId) + val code = userChatRoomInvitationRepository.getInvitationCode(userId,chatRoomId) + val expiration = userChatRoomInvitationRepository.getExpiration(userId,chatRoomId) + return InvitationResponse(code, LocalDateTime.now().plusSeconds(expiration).toString()) } fun verifyChatRoomAccess( From 99bf611f3a526eaab9b0b93ffa16ccb426feca44 Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Wed, 5 Jun 2024 15:49:08 +0900 Subject: [PATCH 033/178] =?UTF-8?q?Header=20=EB=B6=80=EB=B6=84=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Layout/Header.tsx | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/front/src/components/Layout/Header.tsx b/front/src/components/Layout/Header.tsx index a645002a..ed74d0f3 100644 --- a/front/src/components/Layout/Header.tsx +++ b/front/src/components/Layout/Header.tsx @@ -16,8 +16,7 @@ const Header = () => { const DRAWER_WIDTH = 240; // 오른쪽 사이드바 넓이 const [open, setOpen] = useState(false); // 사이드바 열고 닫힌 상태 const [openDrawer, setOpenDrawer] = useState(null); // 메세지 또는 친구 사이드바 상태 - const IsChatRoomShow = useChatRoomStore(state=>state.isChatRoomShow); - + const IsChatRoomShow = useChatRoomStore((state) => state.isChatRoomShow); // 오른쪽 사이드바 오픈 핸들러 const handleDrawerOpen = (sidebar: string) => { @@ -88,25 +87,23 @@ const Header = () => { anchor="right" open={openDrawer === "message"}> - - - - + flexShrink: 0, + "& .MuiDrawer-paper": { + width: DRAWER_WIDTH, + backgroundColor: "#2A2F4F", + color: "white", + }, + }} + variant="persistent" + anchor="right" + open={IsChatRoomShow}> + + ); }; From 18b7cb6da4ee6748e3beb7013e777025b657ffac Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 16:27:05 +0900 Subject: [PATCH 034/178] =?UTF-8?q?rename:=20userChatRoomInvitationReposit?= =?UTF-8?q?ory=EB=A5=BC=20InvitationRepository=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/service/ChatRoomService.kt | 10 +++++----- .../kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index f00b9e05..967831ce 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -2,7 +2,7 @@ package kpring.chat.chatroom.service import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository -import kpring.chat.chatroom.repository.UserChatRoomInvitationRepository +import kpring.chat.chatroom.repository.InvitationRepository import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.response.InvitationResponse @@ -13,7 +13,7 @@ import java.time.LocalDateTime @Service class ChatRoomService( private val chatRoomRepository: ChatRoomRepository, - private val userChatRoomInvitationRepository: UserChatRoomInvitationRepository, + private val invitationRepository: InvitationRepository, ) { fun createChatRoom( request: CreateChatRoomRequest, @@ -39,9 +39,9 @@ class ChatRoomService( userId: String, ): InvitationResponse { verifyChatRoomAccess(chatRoomId, userId) - val code = userChatRoomInvitationRepository.getInvitationCode(userId,chatRoomId) - val expiration = userChatRoomInvitationRepository.getExpiration(userId,chatRoomId) - return InvitationResponse(code, LocalDateTime.now().plusSeconds(expiration).toString()) + val code = invitationRepository.getInvitationCode(userId,chatRoomId) + val expiration = invitationRepository.getExpiration(userId,chatRoomId) + return InvitationResponse(code, expiration) } fun verifyChatRoomAccess( diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt index 55207914..ec43ddaa 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt @@ -7,7 +7,7 @@ import io.mockk.mockk import io.mockk.verify import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository -import kpring.chat.chatroom.repository.UserChatRoomInvitationRepository +import kpring.chat.chatroom.repository.InvitationRepository import kpring.chat.chatroom.service.ChatRoomService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest @@ -17,8 +17,8 @@ import java.util.* class ChatRoomServiceTest : FunSpec({ val chatRoomRepository = mockk() - val userChatRoomInvitationRepository = mockk() - val chatRoomService = ChatRoomService(chatRoomRepository, userChatRoomInvitationRepository) + val invitationRepository = mockk() + val chatRoomService = ChatRoomService(chatRoomRepository, invitationRepository) test("createChatRoom 는 새 ChatRoom을 저장해야 한다") { // Given From ac747fbbd3e83def7ba80157433517030bba17bd Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 16:27:39 +0900 Subject: [PATCH 035/178] =?UTF-8?q?remove:=20InvitationChatRoomRepository?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InvitationChatRoomRepository.kt | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt deleted file mode 100644 index 2b44e3fc..00000000 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationChatRoomRepository.kt +++ /dev/null @@ -1,26 +0,0 @@ -package kpring.chat.chatroom.repository - -import kpring.chat.global.config.PropertyConfig -import kpring.chat.global.exception.ErrorCode -import kpring.chat.global.exception.GlobalException -import org.springframework.data.redis.core.RedisTemplate -import org.springframework.data.redis.core.ValueOperations -import org.springframework.stereotype.Component - -@Component -class InvitationChatRoomRepository( - private val redisTemplate: RedisTemplate, - private val propertyConfig: PropertyConfig, -) { - fun setInvitationCode( - invitationLink: String, - chatRoomId: String, - ) { - val ops: ValueOperations = redisTemplate.opsForValue() - val result = ops.setIfAbsent(invitationLink, chatRoomId, propertyConfig.getExpiration()) - - if (result == null || !result) { - throw GlobalException(ErrorCode.INVITATION_LINK_SAVE_FAILURE) - } - } -} From 18c44efd48aa36eaf6424ae73bc0e58d7eada593 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 16:36:27 +0900 Subject: [PATCH 036/178] =?UTF-8?q?feat:=20invitation=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20=EC=9D=B8=EC=BD=94=EB=94=A9=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EB=8A=94=20method=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...nRepository.kt => InvitationRepository.kt} | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) rename chat/src/main/kotlin/kpring/chat/chatroom/repository/{UserChatRoomInvitationRepository.kt => InvitationRepository.kt} (66%) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt similarity index 66% rename from chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt rename to chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt index 8d1d345d..6a6f03c7 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/UserChatRoomInvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt @@ -4,13 +4,14 @@ import kpring.chat.global.config.PropertyConfig import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.ValueOperations import org.springframework.stereotype.Component +import java.nio.charset.StandardCharsets +import java.security.MessageDigest import java.util.* @Component -class UserChatRoomInvitationRepository( +class InvitationRepository( private val redisTemplate: RedisTemplate, private val propertyConfig: PropertyConfig, - private val invitationChatRoomRepository: InvitationChatRoomRepository, ) { fun getInvitationCode( userId: String, @@ -21,28 +22,37 @@ class UserChatRoomInvitationRepository( if (value == null) { value = setInvitation(key, chatRoomId) } - return value + return generateCode(key, value) } fun setInvitation( key: String, chatRoomId: String, ): String { - val invitationCode = generateCode() + val value = generateValue() val ops: ValueOperations = redisTemplate.opsForValue() - ops.set(key, invitationCode, propertyConfig.getExpiration()) - invitationChatRoomRepository.setInvitationCode(invitationCode, chatRoomId) - return invitationCode + ops.set(key, value, propertyConfig.getExpiration()) + return value } fun getExpiration( userId: String, chatRoomId: String, - ): Long{ + ): Long { val key = chatRoomId return redisTemplate.getExpire(key) } + fun generateCode( + key: String, + value: String, + ): String { + val combine = "$key,$value" + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(combine.toByteArray(StandardCharsets.UTF_8)) + return Base64.getEncoder().encodeToString(hash) + } + private fun generateKey( userId: String, chatRoomId: String, @@ -50,7 +60,7 @@ class UserChatRoomInvitationRepository( return "$userId:$chatRoomId" } - private fun generateCode(): String { + private fun generateValue(): String { return UUID.randomUUID().toString() } } From ee2e9537c6374bb9e404b8eb4688620763fee83d Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Wed, 5 Jun 2024 18:08:07 +0900 Subject: [PATCH 037/178] =?UTF-8?q?[refec]=20=EB=AA=A8=EB=8B=AC=EC=B0=BD?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=EC=B0=BD=20=ED=86=A0=EA=B8=80=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Layout/LeftSideBar.tsx | 20 +++++---- front/src/components/Layout/MemberList.tsx | 41 +++++++++---------- .../components/Layout/ServerInfoSidebar.tsx | 21 +++++----- front/src/components/Modal/ModalComponent.tsx | 21 ++++++++++ .../src/components/Profile/MemberProfile.tsx | 30 -------------- front/src/components/Profile/Profile.tsx | 17 ++++++++ .../components/Server/CreateServerForm.tsx | 18 ++++++++ front/src/hooks/Modal.ts | 11 +++++ front/src/types/layout.ts | 27 ++---------- front/src/types/modal.ts | 12 ++++++ 10 files changed, 125 insertions(+), 93 deletions(-) create mode 100644 front/src/components/Modal/ModalComponent.tsx delete mode 100644 front/src/components/Profile/MemberProfile.tsx create mode 100644 front/src/components/Profile/Profile.tsx create mode 100644 front/src/components/Server/CreateServerForm.tsx create mode 100644 front/src/hooks/Modal.ts create mode 100644 front/src/types/modal.ts diff --git a/front/src/components/Layout/LeftSideBar.tsx b/front/src/components/Layout/LeftSideBar.tsx index 71084821..6f398516 100644 --- a/front/src/components/Layout/LeftSideBar.tsx +++ b/front/src/components/Layout/LeftSideBar.tsx @@ -13,26 +13,29 @@ import AddIcon from "@mui/icons-material/Add"; import { useState } from "react"; import { serverData } from "../../utils/fakeData"; import ServerInfoSidebar from "./ServerInfoSidebar"; +import CreateServerForm from "../Server/CreateServerForm"; +import useModal from "../../hooks/Modal"; const LeftSideBar = () => { const DRAWER_WIDTH = 88; // 왼쪽 서버 사이드바 넓이 - const [open, setOpen] = useState(false); // 서버 인포 사이드바 열 + const [openServerInfo, setOpenServerInfo] = useState(false); // 서버 인포 사이드바 열기 + const { isOpen, openModal } = useModal(); const [serverId, setServerId] = useState(""); // 왼쪽 멤버 사이드바 오픈 핸들러 const handleDrawerOpen = (id: string) => { - setOpen(true); + setOpenServerInfo((openServerInfo) => !openServerInfo); setServerId(id); }; // 왼쪽 멤버 사이드바 닫기 핸들러 const handleDrawerClose = () => { - setOpen(false); + setOpenServerInfo((openServerInfo) => !openServerInfo); }; return ( - {/* { - + @@ -71,13 +74,14 @@ const LeftSideBar = () => { })} - + - */} + + ); }; diff --git a/front/src/components/Layout/MemberList.tsx b/front/src/components/Layout/MemberList.tsx index b1518bf4..19e86539 100644 --- a/front/src/components/Layout/MemberList.tsx +++ b/front/src/components/Layout/MemberList.tsx @@ -1,33 +1,32 @@ import { List } from "@mui/material"; import MemberListItem from "./MemberListItem"; -import MemberProfile from "../Profile/MemberProfile"; import React from "react"; import { Member } from "../../types/layout"; +import ModalComponent from "../Modal/ModalComponent"; +import Profile from "../Profile/Profile"; +import useModal from "../../hooks/Modal"; -interface MemberListProps{ - memberList: Member[] +interface MemberListProps { + memberList: Member[]; } // TODO : 오른쪽 사이드바 멤버 리스트 -const MemberList : React.FC = ({memberList}) => { - const [openProfile, setOpenProfile] = React.useState(false); - const handleProfileOpen = () => setOpenProfile(true); - const handleProfileClose = () => setOpenProfile(false); +const MemberList: React.FC = ({ memberList }) => { + const { isOpen, openModal, closeModal } = useModal(); - return ( + return ( <> - - {memberList.map(member=>( - - ))} - - + + {memberList.map((member) => ( + + ))} + + + {/* TODO: API연결되면 props로 유저정보 내려주기 */} + + + ); +}; - ) -} - -export default MemberList \ No newline at end of file +export default MemberList; diff --git a/front/src/components/Layout/ServerInfoSidebar.tsx b/front/src/components/Layout/ServerInfoSidebar.tsx index 6ad9c7d6..52bee367 100644 --- a/front/src/components/Layout/ServerInfoSidebar.tsx +++ b/front/src/components/Layout/ServerInfoSidebar.tsx @@ -10,16 +10,20 @@ import { ListItemText, Button, styled, + Modal, } from "@mui/material"; import { serverData } from "../../utils/fakeData"; -import MemberProfile from "../Profile/MemberProfile"; +import MemberProfile from "../Profile/Profile"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import { useNavigate } from "react-router-dom"; import FavoriteStar from "../Home/FavoriteStar"; +import ModalComponent from "../Modal/ModalComponent"; +import useModal from "../../hooks/Modal"; +import Profile from "../Profile/Profile"; const ServerInfoSidebar: React.FC = ({ close, - open, + // open, serverID, }) => { const DrawerHeader = styled("div")(({ theme }) => ({ @@ -29,11 +33,8 @@ const ServerInfoSidebar: React.FC = ({ ...theme.mixins.toolbar, justifyContent: "flex-start", })); - - const [openProfile, setOpenProfile] = React.useState(false); - const handleOpen = () => setOpenProfile(true); - const handleClose = () => setOpenProfile(false); const navigate = useNavigate(); + const { isOpen, openModal, closeModal } = useModal(); return ( <> @@ -50,7 +51,7 @@ const ServerInfoSidebar: React.FC = ({ .map((member) => { return ( - + = ({ ); })} - + + + ); }; diff --git a/front/src/components/Modal/ModalComponent.tsx b/front/src/components/Modal/ModalComponent.tsx new file mode 100644 index 00000000..8b4bd11c --- /dev/null +++ b/front/src/components/Modal/ModalComponent.tsx @@ -0,0 +1,21 @@ +import { Box, Modal } from "@mui/material"; +import React, { ReactNode } from "react"; +import { modalStyle } from "../../types/modal"; + +interface ModalComponentProps { + children: ReactNode; + isOpen: boolean; +} + +const ModalComponent = ({ children, isOpen }: ModalComponentProps) => { + return ( + + {children} + + ); +}; + +export default ModalComponent; diff --git a/front/src/components/Profile/MemberProfile.tsx b/front/src/components/Profile/MemberProfile.tsx deleted file mode 100644 index a9cc85ae..00000000 --- a/front/src/components/Profile/MemberProfile.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Avatar, Box, Button, Modal, Typography } from "@mui/material"; -import React from "react"; -import { - MemberProfileModalProps, - memberProfileModalStyle, -} from "../../types/layout"; - -const MemberProfile: React.FC = ({ - openModal, - closeModal, -}) => { - return ( -
- - - - User Name - - - -
- ); -}; - -export default MemberProfile; diff --git a/front/src/components/Profile/Profile.tsx b/front/src/components/Profile/Profile.tsx new file mode 100644 index 00000000..dc61338b --- /dev/null +++ b/front/src/components/Profile/Profile.tsx @@ -0,0 +1,17 @@ +import { Avatar, Button, Typography } from "@mui/material"; + +interface ProfileProps { + closeModal: () => void; +} + +const Profile = ({ closeModal }: ProfileProps) => { + return ( + <> + + User Name + + + ); +}; + +export default Profile; diff --git a/front/src/components/Server/CreateServerForm.tsx b/front/src/components/Server/CreateServerForm.tsx new file mode 100644 index 00000000..2b32450c --- /dev/null +++ b/front/src/components/Server/CreateServerForm.tsx @@ -0,0 +1,18 @@ +import { Box, Button, Modal, Typography } from "@mui/material"; +import React from "react"; +import useModal from "../../hooks/Modal"; + +const CreateServerForm = () => { + const { isOpen, closeModal, openModal } = useModal(); + + return ( +
+
+ + +
+
+ ); +}; + +export default CreateServerForm; diff --git a/front/src/hooks/Modal.ts b/front/src/hooks/Modal.ts new file mode 100644 index 00000000..58e57dcc --- /dev/null +++ b/front/src/hooks/Modal.ts @@ -0,0 +1,11 @@ +import { useState } from "react"; + +const useModal = () => { + const [isOpen, setIsOpen] = useState(false); + const openModal = () => setIsOpen(true); + const closeModal = () => setIsOpen(false); + + return { isOpen, openModal, closeModal }; +}; + +export default useModal; diff --git a/front/src/types/layout.ts b/front/src/types/layout.ts index 80d52eae..07d979ce 100644 --- a/front/src/types/layout.ts +++ b/front/src/types/layout.ts @@ -10,30 +10,9 @@ export interface ServerInforProps { serverID: string; } -// 멤버 프로필 모달 스타일 -export const memberProfileModalStyle = { - position: "absolute" as "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 400, - bgcolor: "backround.paper", - border: "2px solid #000", - boxShadow: 24, - p: 4, -}; - -// 멤버 프로필 모달 -export interface MemberProfileModalProps { - openModal: boolean; - closeModal: () => void; -} - -// FIXME :멤버 조회 타입 임의로 지정(이후 API 명세서 수정에 따라 변경 필요) -export interface Member{ - +// FIXME :멤버 조회 타입 임의로 지정(이후 API 명세서 수정에 따라 변경 필요) +export interface Member { id: number; profilePath: string; userName: string; - -} \ No newline at end of file +} diff --git a/front/src/types/modal.ts b/front/src/types/modal.ts new file mode 100644 index 00000000..72b3330b --- /dev/null +++ b/front/src/types/modal.ts @@ -0,0 +1,12 @@ +// 멤버 프로필 모달 스타일 +export const modalStyle = { + position: "absolute" as "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 400, + bgcolor: "backround.paper", + border: "2px solid #000", + boxShadow: 24, + p: 4, +}; From a45b609f475391b0cd2740f98988db3e380c829c Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Wed, 5 Jun 2024 20:27:12 +0900 Subject: [PATCH 038/178] =?UTF-8?q?[feat]=20server=20form=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Layout/LeftSideBar.tsx | 5 ++- .../components/Server/CreateServerForm.tsx | 32 +++++++++++++++--- infra/Chart.lock | 6 ++++ infra/charts/ingress-nginx-4.10.1.tgz | Bin 0 -> 55564 bytes 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 infra/Chart.lock create mode 100644 infra/charts/ingress-nginx-4.10.1.tgz diff --git a/front/src/components/Layout/LeftSideBar.tsx b/front/src/components/Layout/LeftSideBar.tsx index 6f398516..46ec0123 100644 --- a/front/src/components/Layout/LeftSideBar.tsx +++ b/front/src/components/Layout/LeftSideBar.tsx @@ -15,6 +15,7 @@ import { serverData } from "../../utils/fakeData"; import ServerInfoSidebar from "./ServerInfoSidebar"; import CreateServerForm from "../Server/CreateServerForm"; import useModal from "../../hooks/Modal"; +import ModalComponent from "../Modal/ModalComponent"; const LeftSideBar = () => { const DRAWER_WIDTH = 88; // 왼쪽 서버 사이드바 넓이 @@ -81,7 +82,9 @@ const LeftSideBar = () => { serverID={serverId} /> - + + + ); }; diff --git a/front/src/components/Server/CreateServerForm.tsx b/front/src/components/Server/CreateServerForm.tsx index 2b32450c..87c93bc6 100644 --- a/front/src/components/Server/CreateServerForm.tsx +++ b/front/src/components/Server/CreateServerForm.tsx @@ -1,15 +1,39 @@ -import { Box, Button, Modal, Typography } from "@mui/material"; +import { Button, FormControl, Input, Select } from "@mui/material"; + import React from "react"; import useModal from "../../hooks/Modal"; +import { Label } from "@mui/icons-material"; +import axios from "axios"; const CreateServerForm = () => { - const { isOpen, closeModal, openModal } = useModal(); + const { closeModal, openModal } = useModal(); + + const onSubmitHandler = async () => {}; return (
-
+

새로운 서버 생성

+ - + + + {/* + + + + + + + + */} +
); diff --git a/infra/Chart.lock b/infra/Chart.lock new file mode 100644 index 00000000..14319018 --- /dev/null +++ b/infra/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: ingress-nginx + repository: https://kubernetes.github.io/ingress-nginx + version: 4.10.1 +digest: sha256:d25ff4f8cab798c10c0cf44b9cac931287596320e9645bd294dc9eaaa5717bd9 +generated: "2024-06-05T19:52:15.651144+09:00" diff --git a/infra/charts/ingress-nginx-4.10.1.tgz b/infra/charts/ingress-nginx-4.10.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..278d0a5405f0baa814f05000ae8891357f2e4652 GIT binary patch literal 55564 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 wre29#+&9{7jIOr@t&KB+LUGc52~(yJNJ Date: Wed, 5 Jun 2024 21:03:21 +0900 Subject: [PATCH 039/178] =?UTF-8?q?chore=20:=20docker=20hub=20auth=20appli?= =?UTF-8?q?cation=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 2 +- infra/charts/auth/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index c255935c..7d03df23 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -84,7 +84,7 @@ jib { image = "eclipse-temurin:21-jre" } to { - image = "localhost:5000/auth-application" + image = "youdong98/kpring-auth-application" setAllowInsecureRegistries(true) tags = setOf("latest") } diff --git a/infra/charts/auth/values.yaml b/infra/charts/auth/values.yaml index 6f4adb54..86222375 100644 --- a/infra/charts/auth/values.yaml +++ b/infra/charts/auth/values.yaml @@ -1,7 +1,7 @@ # auth service service: name: auth-application - image: localhost:5000/auth-application:latest + image: youdong98/kpring-auth-application:latest port: 8080 nodePort: 30001 jwt: From dbef416cb330e3c88eba412e181a25d2aad0dd2f Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 5 Jun 2024 21:07:30 +0900 Subject: [PATCH 040/178] =?UTF-8?q?chore=20:=20docker=20hub=20server=20app?= =?UTF-8?q?lication=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/charts/server/values.yaml | 2 +- server/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/charts/server/values.yaml b/infra/charts/server/values.yaml index 5a260e38..52b3f6fe 100644 --- a/infra/charts/server/values.yaml +++ b/infra/charts/server/values.yaml @@ -1,7 +1,7 @@ # server service service: name: server-application - image: localhost:5000/server-application:latest + image: youdong98/kpring-server-application:latest port: 8080 nodePort: 30003 diff --git a/server/build.gradle.kts b/server/build.gradle.kts index b0989482..3cb9a6cd 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -81,7 +81,7 @@ jib { image = "eclipse-temurin:21-jre" } to { - image = "localhost:5000/server-application" + image = "youdong98/kpring-server-application" setAllowInsecureRegistries(true) tags = setOf("latest") } From faee0e77ee4a14b5b60a49bc6d7c98a891f6168f Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 5 Jun 2024 21:10:04 +0900 Subject: [PATCH 041/178] =?UTF-8?q?chore=20:=20docker=20hub=20user=20appli?= =?UTF-8?q?cation=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/charts/user/values.yaml | 2 +- user/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/charts/user/values.yaml b/infra/charts/user/values.yaml index 76d01e7b..c393a713 100644 --- a/infra/charts/user/values.yaml +++ b/infra/charts/user/values.yaml @@ -1,7 +1,7 @@ # user service service: name: user-application - image: localhost:5000/user-application:latest + image: youdong98/kpring-user-application:latest port: 8080 nodePort: 30002 diff --git a/user/build.gradle.kts b/user/build.gradle.kts index db4ef108..8f89c009 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -65,7 +65,7 @@ jib { image = "eclipse-temurin:21-jre" } to { - image = "localhost:5000/user-application" + image = "youdong98/kpring-user-application" setAllowInsecureRegistries(true) tags = setOf("latest") } From 1854afde9b8bc7a96cbd0cb67f6846dd34ed82ea Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 22:02:13 +0900 Subject: [PATCH 042/178] =?UTF-8?q?fix:=20getExpire=EB=A5=BC=20=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20key=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/repository/InvitationRepository.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt index 6a6f03c7..10597f1b 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt @@ -39,8 +39,9 @@ class InvitationRepository( userId: String, chatRoomId: String, ): Long { - val key = chatRoomId - return redisTemplate.getExpire(key) + val key = generateKey(userId, chatRoomId) + val expiration = redisTemplate.getExpire(key) + return expiration } fun generateCode( From 6020df35483ac23f539ce62786eb7242321c83c0 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 22:21:11 +0900 Subject: [PATCH 043/178] =?UTF-8?q?refac:=20InvitationResponse=EC=97=90?= =?UTF-8?q?=EC=84=9C=20expiration=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chatroom/service/ChatRoomService.kt | 6 ++---- .../core/chat/chat/dto/response/InvitationResponse.kt | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index 967831ce..d95e50b4 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -8,7 +8,6 @@ import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.response.InvitationResponse import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest import org.springframework.stereotype.Service -import java.time.LocalDateTime @Service class ChatRoomService( @@ -39,9 +38,8 @@ class ChatRoomService( userId: String, ): InvitationResponse { verifyChatRoomAccess(chatRoomId, userId) - val code = invitationRepository.getInvitationCode(userId,chatRoomId) - val expiration = invitationRepository.getExpiration(userId,chatRoomId) - return InvitationResponse(code, expiration) + val code = invitationRepository.getInvitationCode(userId, chatRoomId) + return InvitationResponse(code) } fun verifyChatRoomAccess( diff --git a/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt index fafc7c4a..70f2823a 100644 --- a/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt +++ b/core/src/main/kotlin/kpring/core/chat/chat/dto/response/InvitationResponse.kt @@ -2,5 +2,4 @@ package kpring.core.chat.chat.dto.response data class InvitationResponse( val code: String, - val expiration: String, ) From 2383b4017bc9128761d802953686b01044d999b6 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 22:24:56 +0900 Subject: [PATCH 044/178] =?UTF-8?q?rename:=20ChatRoomProperty=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/repository/InvitationRepository.kt | 6 +++--- .../config/{PropertyConfig.kt => ChatRoomProperty.kt} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename chat/src/main/kotlin/kpring/chat/global/config/{PropertyConfig.kt => ChatRoomProperty.kt} (94%) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt index 10597f1b..45dbef7c 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt @@ -1,6 +1,6 @@ package kpring.chat.chatroom.repository -import kpring.chat.global.config.PropertyConfig +import kpring.chat.global.config.ChatRoomProperty import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.ValueOperations import org.springframework.stereotype.Component @@ -11,7 +11,7 @@ import java.util.* @Component class InvitationRepository( private val redisTemplate: RedisTemplate, - private val propertyConfig: PropertyConfig, + private val chatRoomProperty: ChatRoomProperty, ) { fun getInvitationCode( userId: String, @@ -31,7 +31,7 @@ class InvitationRepository( ): String { val value = generateValue() val ops: ValueOperations = redisTemplate.opsForValue() - ops.set(key, value, propertyConfig.getExpiration()) + ops.set(key, value, chatRoomProperty.getExpiration()) return value } diff --git a/chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt b/chat/src/main/kotlin/kpring/chat/global/config/ChatRoomProperty.kt similarity index 94% rename from chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt rename to chat/src/main/kotlin/kpring/chat/global/config/ChatRoomProperty.kt index a723b30e..4207c97c 100644 --- a/chat/src/main/kotlin/kpring/chat/global/config/PropertyConfig.kt +++ b/chat/src/main/kotlin/kpring/chat/global/config/ChatRoomProperty.kt @@ -6,7 +6,7 @@ import java.time.Duration @Configuration @ConfigurationProperties(prefix = "chatroom") -class PropertyConfig { +class ChatRoomProperty { private lateinit var expiration: Duration fun getExpiration(): Duration { From be2f9619fab4fb922958c0aed50eb69de303e680 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 5 Jun 2024 22:34:11 +0900 Subject: [PATCH 045/178] =?UTF-8?q?refac:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt index d374cd1d..90ba6067 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -26,7 +26,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext -import java.time.LocalDateTime @WebMvcTest(controllers = [ChatRoomController::class]) @ExtendWith(RestDocumentationExtension::class) @@ -59,8 +58,7 @@ class ChatRoomControllerTest( val chatRoomId = ChatRoomTest.TEST_ROOM_ID val userId = CommonTest.TEST_USER_ID val key = "62e9df6b-13cb-4673-a6fe-8566451b7f15" - val expiration = LocalDateTime.now().plusSeconds(3600).toString() - val data = InvitationResponse(key, expiration) + val data = InvitationResponse(key) every { authClient.getTokenInfo(any()) } returns ApiResponse( @@ -96,7 +94,6 @@ class ChatRoomControllerTest( response { body { "data.code" type JsonDataType.Strings mean "참여 코드" - "data.expiration" type JsonDataType.Strings mean "참여코드 유효시간" } } } From 640d3ac6c1aea8a7df347d8cc4645651540417a8 Mon Sep 17 00:00:00 2001 From: minisun Date: Thu, 6 Jun 2024 00:18:40 +0900 Subject: [PATCH 046/178] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/core/chat/chat/dto/request/ChatType.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/kpring/core/chat/chat/dto/request/ChatType.kt b/core/src/main/kotlin/kpring/core/chat/chat/dto/request/ChatType.kt index 8093852f..46dc1438 100644 --- a/core/src/main/kotlin/kpring/core/chat/chat/dto/request/ChatType.kt +++ b/core/src/main/kotlin/kpring/core/chat/chat/dto/request/ChatType.kt @@ -2,9 +2,10 @@ package kpring.core.chat.chat.dto.request enum class ChatType(val type: String) { Room("Room"), - Server("Server"); + Server("Server"), + ; - override fun toString(): String{ - return type; + override fun toString(): String { + return type } } From 7804e427df1dba039753f1130357cba6c61c61fd Mon Sep 17 00:00:00 2001 From: minisun Date: Thu, 6 Jun 2024 22:23:01 +0900 Subject: [PATCH 047/178] =?UTF-8?q?feat:=20InvitationService=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/service/InvitationService.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt new file mode 100644 index 00000000..6f7ae4d7 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt @@ -0,0 +1,73 @@ +package kpring.chat.chatroom.service + +import kpring.chat.chatroom.repository.InvitationRepository +import kpring.chat.global.config.ChatRoomProperty +import org.springframework.stereotype.Service +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.* + +@Service +class InvitationService( + private val invitationRepository: InvitationRepository, + private val chatRoomProperty: ChatRoomProperty, +) { + fun getOrCreateInvitation( + userId: String, + chatRoomId: String, + ): String { + var code = getInvitation(userId, chatRoomId) + if (code == null) { + code = setInvitation(userId, chatRoomId) + } + return code + } + + private fun getInvitation( + userId: String, + chatRoomId: String, + ): String? { + val key = generateKey(userId, chatRoomId) + return invitationRepository.getValue(key) + } + + private fun setInvitation( + userId: String, + chatRoomId: String, + ): String { + val key = generateKey(userId, chatRoomId) + val value = generateValue() + invitationRepository.setValueAndExpiration(key, value, chatRoomProperty.getExpiration()) + return value + } + + fun generateKeyAndCode( + userId: String, + chatRoomId: String, + code: String, + ): String { + val key = generateKey(userId, chatRoomId) + return generateCode(key, code) + } + + private fun generateCode( + key: String, + value: String, + ): String { + val combinedString = "$key,$value" + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(combinedString.toByteArray(StandardCharsets.UTF_8)) + return Base64.getEncoder().encodeToString(hash) + } + + private fun generateKey( + userId: String, + chatRoomId: String, + ): String { + return "$userId:$chatRoomId" + } + + private fun generateValue(): String { + return UUID.randomUUID().toString() + } +} From 0449e6f4666e85443e60769b033ed90fbeec845b Mon Sep 17 00:00:00 2001 From: minisun Date: Thu, 6 Jun 2024 22:23:28 +0900 Subject: [PATCH 048/178] =?UTF-8?q?refac:=20InvitationRepository=EC=9D=98?= =?UTF-8?q?=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EB=B6=88=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20InvitationService=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/InvitationRepository.kt | 59 ++++--------------- 1 file changed, 10 insertions(+), 49 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt index 45dbef7c..5e1fd019 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/repository/InvitationRepository.kt @@ -1,67 +1,28 @@ package kpring.chat.chatroom.repository -import kpring.chat.global.config.ChatRoomProperty import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.ValueOperations import org.springframework.stereotype.Component -import java.nio.charset.StandardCharsets -import java.security.MessageDigest -import java.util.* +import java.time.Duration @Component class InvitationRepository( private val redisTemplate: RedisTemplate, - private val chatRoomProperty: ChatRoomProperty, ) { - fun getInvitationCode( - userId: String, - chatRoomId: String, - ): String { - val key = generateKey(userId, chatRoomId) - var value = redisTemplate.opsForValue().get(key) - if (value == null) { - value = setInvitation(key, chatRoomId) - } - return generateCode(key, value) + fun getValue(key: String): String? { + return redisTemplate.opsForValue().get(key) } - fun setInvitation( - key: String, - chatRoomId: String, - ): String { - val value = generateValue() - val ops: ValueOperations = redisTemplate.opsForValue() - ops.set(key, value, chatRoomProperty.getExpiration()) - return value - } - - fun getExpiration( - userId: String, - chatRoomId: String, - ): Long { - val key = generateKey(userId, chatRoomId) - val expiration = redisTemplate.getExpire(key) - return expiration - } - - fun generateCode( + fun setValueAndExpiration( key: String, value: String, - ): String { - val combine = "$key,$value" - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(combine.toByteArray(StandardCharsets.UTF_8)) - return Base64.getEncoder().encodeToString(hash) - } - - private fun generateKey( - userId: String, - chatRoomId: String, - ): String { - return "$userId:$chatRoomId" + expiration: Duration, + ) { + val ops: ValueOperations = redisTemplate.opsForValue() + ops.set(key, value, expiration) } - private fun generateValue(): String { - return UUID.randomUUID().toString() + fun getExpiration(key: String): Long { + return redisTemplate.getExpire(key) } } From df2ecfdabe73ea504d7b95b7b629f158b22bf29f Mon Sep 17 00:00:00 2001 From: minisun Date: Thu, 6 Jun 2024 22:23:46 +0900 Subject: [PATCH 049/178] =?UTF-8?q?refac:=20InvitationRepository=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20InvitationService=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/service/ChatRoomService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index d95e50b4..b4f37186 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -2,7 +2,6 @@ package kpring.chat.chatroom.service import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository -import kpring.chat.chatroom.repository.InvitationRepository import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.response.InvitationResponse @@ -12,7 +11,7 @@ import org.springframework.stereotype.Service @Service class ChatRoomService( private val chatRoomRepository: ChatRoomRepository, - private val invitationRepository: InvitationRepository, + private val invitationService: InvitationService, ) { fun createChatRoom( request: CreateChatRoomRequest, @@ -38,8 +37,9 @@ class ChatRoomService( userId: String, ): InvitationResponse { verifyChatRoomAccess(chatRoomId, userId) - val code = invitationRepository.getInvitationCode(userId, chatRoomId) - return InvitationResponse(code) + val code = invitationService.getOrCreateInvitation(userId, chatRoomId) + val encodedCode = invitationService.generateKeyAndCode(userId, chatRoomId, code) + return InvitationResponse(encodedCode) } fun verifyChatRoomAccess( From c583e10c73aa49e17c94a5c47eb2c3308e0b9ab1 Mon Sep 17 00:00:00 2001 From: minisun Date: Thu, 6 Jun 2024 22:24:19 +0900 Subject: [PATCH 050/178] =?UTF-8?q?chore:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?dependency=20ChatRoomServiceTest=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt index ec43ddaa..8296daa6 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt @@ -7,8 +7,8 @@ import io.mockk.mockk import io.mockk.verify import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository -import kpring.chat.chatroom.repository.InvitationRepository import kpring.chat.chatroom.service.ChatRoomService +import kpring.chat.chatroom.service.InvitationService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest import kpring.core.chat.chatroom.dto.request.CreateChatRoomRequest @@ -17,8 +17,8 @@ import java.util.* class ChatRoomServiceTest : FunSpec({ val chatRoomRepository = mockk() - val invitationRepository = mockk() - val chatRoomService = ChatRoomService(chatRoomRepository, invitationRepository) + val invitationService = mockk() + val chatRoomService = ChatRoomService(chatRoomRepository, invitationService) test("createChatRoom 는 새 ChatRoom을 저장해야 한다") { // Given From 84a00795f050a8104d41f6112ad4d02f436b7f42 Mon Sep 17 00:00:00 2001 From: Jihyun Jeon <80092348+jihyun-j@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:38:01 +0900 Subject: [PATCH 051/178] Delete infra/Chart.lock --- infra/Chart.lock | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 infra/Chart.lock diff --git a/infra/Chart.lock b/infra/Chart.lock deleted file mode 100644 index 14319018..00000000 --- a/infra/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: ingress-nginx - repository: https://kubernetes.github.io/ingress-nginx - version: 4.10.1 -digest: sha256:d25ff4f8cab798c10c0cf44b9cac931287596320e9645bd294dc9eaaa5717bd9 -generated: "2024-06-05T19:52:15.651144+09:00" From f2732db757566755d68d568221c31a4db68a11d4 Mon Sep 17 00:00:00 2001 From: Jihyun Jeon <80092348+jihyun-j@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:45:07 +0900 Subject: [PATCH 052/178] =?UTF-8?q?Chart.lock=20=ED=8C=8C=EC=9D=BC=20.giti?= =?UTF-8?q?gnore=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/front/.gitignore b/front/.gitignore index 4d29575d..c5e54f8a 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -21,3 +21,4 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +Chart.lock From f26e8c3c55b635f23cf60aee4c71f1520402a526 Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Fri, 7 Jun 2024 17:06:04 +0900 Subject: [PATCH 053/178] =?UTF-8?q?[refec]=20=EC=84=9C=EB=B2=84=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B4=80=EB=A0=A8=20=ED=83=80=EC=9E=85=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Auth/LoginBox.tsx | 9 ++-- front/src/components/Home/FavoriteStar.tsx | 2 +- front/src/components/Home/ServerCard.tsx | 20 +++----- front/src/components/Home/ServerCardList.tsx | 19 +++----- front/src/components/Map/ServerMap.tsx | 2 +- front/src/components/Map/ServerMap/Debug.ts | 2 +- front/src/components/Map/ServerMap/Layers.ts | 2 +- front/src/components/Modal/ModalComponent.tsx | 4 +- .../components/Server/CreateServerForm.tsx | 46 +++++++++++++++++-- front/src/hooks/FavoriteServer.ts | 2 +- .../store.ts => store/useFavoriteStore.ts} | 0 front/src/{types => style}/modal.ts | 0 front/src/types/map.ts | 29 ++++++++++++ front/src/types/server.ts | 33 ++++--------- 14 files changed, 102 insertions(+), 68 deletions(-) rename front/src/{stores/store.ts => store/useFavoriteStore.ts} (100%) rename front/src/{types => style}/modal.ts (100%) create mode 100644 front/src/types/map.ts diff --git a/front/src/components/Auth/LoginBox.tsx b/front/src/components/Auth/LoginBox.tsx index 645309a2..29c7b62b 100644 --- a/front/src/components/Auth/LoginBox.tsx +++ b/front/src/components/Auth/LoginBox.tsx @@ -63,8 +63,7 @@ function LoginBox() { " border="1px solid #e4d4e7" padding="20px" - onSubmit={clickSubmitHandler} - > + onSubmit={clickSubmitHandler}>

디코타운에 어서오세요!

@@ -100,8 +99,7 @@ function LoginBox() { type="submit" variant="contained" startIcon={} - sx={{ width: "90%" }} - > + sx={{ width: "90%" }}> 로그인 @@ -110,8 +108,7 @@ function LoginBox() { color="secondary" startIcon={} sx={{ mt: "20px", width: "90%", mb: "20px" }} - onClick={() => navigation("/join")} - > + onClick={() => navigation("/join")}> 회원가입 diff --git a/front/src/components/Home/FavoriteStar.tsx b/front/src/components/Home/FavoriteStar.tsx index fb0565e5..13fc515d 100644 --- a/front/src/components/Home/FavoriteStar.tsx +++ b/front/src/components/Home/FavoriteStar.tsx @@ -1,7 +1,7 @@ import StarBorderRoundedIcon from "@mui/icons-material/StarBorderRounded"; import StarRoundedIcon from "@mui/icons-material/StarRounded"; import { useIsFavorite } from "../../hooks/FavoriteServer"; -import useFavoriteStore from "../../stores/store"; +import useFavoriteStore from "../../store/useFavoriteStore"; const FavoriteStar = ({ id }: { id: string }) => { const isFavorite = useIsFavorite(id); diff --git a/front/src/components/Home/ServerCard.tsx b/front/src/components/Home/ServerCard.tsx index 377fa844..45665012 100644 --- a/front/src/components/Home/ServerCard.tsx +++ b/front/src/components/Home/ServerCard.tsx @@ -1,24 +1,16 @@ import { Card, CardContent, CardMedia, Typography } from "@mui/material"; import FavoriteStar from "./FavoriteStar"; +import { ServerCardProps } from "../../types/server"; -const ServerCard = ({ - servers, -}: { - servers: { - serverId: string; - serverName: string; - image: string; - members: string[]; - }; -}) => { +const ServerCard: React.FC = ({ server }) => { return (
- + - {servers.serverName} - {servers.members.length}명 - + {server.serverName} + {server.members.length}명 +
diff --git a/front/src/components/Home/ServerCardList.tsx b/front/src/components/Home/ServerCardList.tsx index 6feff9d8..e4f258a9 100644 --- a/front/src/components/Home/ServerCardList.tsx +++ b/front/src/components/Home/ServerCardList.tsx @@ -1,23 +1,16 @@ import ServerCard from "./ServerCard"; import { useFilterFavorites } from "../../hooks/FavoriteServer"; +import { ServerCardListProps, ServerType } from "../../types/server"; -const ServerCardList = ({ - servers, -}: { - servers: { - serverId: string; - serverName: string; - image: string; - members: string[]; - }[]; -}) => { +const ServerCardList: React.FC = ({ servers }) => { const favoriteItems = useFilterFavorites(); + return ( <> {servers - .filter((server) => favoriteItems.includes(server.serverId)) - .map((server) => ( - + .filter((server: ServerType) => favoriteItems.includes(server.serverId)) + .map((server: ServerType) => ( + ))} ); diff --git a/front/src/components/Map/ServerMap.tsx b/front/src/components/Map/ServerMap.tsx index bc1940b8..45f8c9bb 100644 --- a/front/src/components/Map/ServerMap.tsx +++ b/front/src/components/Map/ServerMap.tsx @@ -1,6 +1,6 @@ import { forwardRef, useEffect, useLayoutEffect, useRef } from "react"; import { EventBus } from "./EventBus"; -import { ServerMapProps, ServerMapTypes } from "../../types/server"; +import { ServerMapProps, ServerMapTypes } from "../../types/map"; import EnterServer from "./main"; // 서버를 생성하고 관리하는 컴포넌트 diff --git a/front/src/components/Map/ServerMap/Debug.ts b/front/src/components/Map/ServerMap/Debug.ts index 86a4ca65..e4f96783 100644 --- a/front/src/components/Map/ServerMap/Debug.ts +++ b/front/src/components/Map/ServerMap/Debug.ts @@ -1,7 +1,7 @@ // 이 파일은 캐릭터와 물체의 충돌을 확인하기 위한 디버그 파일입니다. import { Scene } from "phaser"; -import { Layers } from "../../../types/server"; +import { Layers } from "../../../types/map"; export const debugCollision = (scene: Scene, layers: Layers) => { const collidableLayers = [ diff --git a/front/src/components/Map/ServerMap/Layers.ts b/front/src/components/Map/ServerMap/Layers.ts index ca480885..3846c430 100644 --- a/front/src/components/Map/ServerMap/Layers.ts +++ b/front/src/components/Map/ServerMap/Layers.ts @@ -1,5 +1,5 @@ import { Scene } from "phaser"; -import { Layers } from "../../../types/server"; +import { Layers } from "../../../types/map"; export const createLayers = (map: Phaser.Tilemaps.Tilemap, scene: Scene) => { // Tiled에서 그린 잔디, 집, 나무 등과 같은 타일 요소들을 화면에 뿌려준다. diff --git a/front/src/components/Modal/ModalComponent.tsx b/front/src/components/Modal/ModalComponent.tsx index 8b4bd11c..a0ddfa0f 100644 --- a/front/src/components/Modal/ModalComponent.tsx +++ b/front/src/components/Modal/ModalComponent.tsx @@ -1,6 +1,6 @@ import { Box, Modal } from "@mui/material"; -import React, { ReactNode } from "react"; -import { modalStyle } from "../../types/modal"; +import { ReactNode } from "react"; +import { modalStyle } from "../../style/modal"; interface ModalComponentProps { children: ReactNode; diff --git a/front/src/components/Server/CreateServerForm.tsx b/front/src/components/Server/CreateServerForm.tsx index 87c93bc6..f2c9b6dc 100644 --- a/front/src/components/Server/CreateServerForm.tsx +++ b/front/src/components/Server/CreateServerForm.tsx @@ -1,14 +1,51 @@ -import { Button, FormControl, Input, Select } from "@mui/material"; +import { Input } from "@mui/material"; -import React from "react"; +import React, { useState } from "react"; import useModal from "../../hooks/Modal"; -import { Label } from "@mui/icons-material"; + import axios from "axios"; const CreateServerForm = () => { const { closeModal, openModal } = useModal(); + const [jwtToken, setJwtToken] = useState(""); + + console.log(jwtToken); + + const onSubmitHandler = async (e: React.FormEvent) => { + e.preventDefault(); + const SERVER_URL = "http://localhost/server/api/v1/server"; + + try { + const res = await axios.post( + SERVER_URL, + { + serverName: "TEST", + }, + { + headers: { + Authorization: `Bearer ${jwtToken}`, + }, + } + ); + console.log(res); + } catch (error) { + console.error(error); + } + }; + + const tempLogin = async () => { + const SERVER_URL = "http://localhost/user/api/v1/login"; - const onSubmitHandler = async () => {}; + try { + const res = await axios.post(SERVER_URL, { + email: "test@email.com", + password: "tesT@1234", + }); + setJwtToken(res.data.data.accessToken); + } catch (error) { + console.error(error); + } + }; return (
@@ -35,6 +72,7 @@ const CreateServerForm = () => { */} +
); }; diff --git a/front/src/hooks/FavoriteServer.ts b/front/src/hooks/FavoriteServer.ts index ec32232d..5e5518cf 100644 --- a/front/src/hooks/FavoriteServer.ts +++ b/front/src/hooks/FavoriteServer.ts @@ -1,4 +1,4 @@ -import useFavoriteStore from "../stores/store"; +import useFavoriteStore from "../store/useFavoriteStore"; export const useIsFavorite = (id: string) => { return useFavoriteStore((state) => state.favorites[id] ?? false); diff --git a/front/src/stores/store.ts b/front/src/store/useFavoriteStore.ts similarity index 100% rename from front/src/stores/store.ts rename to front/src/store/useFavoriteStore.ts diff --git a/front/src/types/modal.ts b/front/src/style/modal.ts similarity index 100% rename from front/src/types/modal.ts rename to front/src/style/modal.ts diff --git a/front/src/types/map.ts b/front/src/types/map.ts new file mode 100644 index 00000000..e198f489 --- /dev/null +++ b/front/src/types/map.ts @@ -0,0 +1,29 @@ +export interface ServerMapTypes { + server: Phaser.Game | null; + scene: Phaser.Scene | null; +} + +// 현재 활성화된 씬을 부모 컴포넌트로 전달하는 콜백 함수 +export interface ServerMapProps { + currentActiveScene?: (scene_instance: Phaser.Scene) => void; +} + +// Tiled Map 레이어 타입 +export interface Layers { + mapLayer?: Phaser.Tilemaps.TilemapLayer | null; + groundLayer?: Phaser.Tilemaps.TilemapLayer | null; + chickHouseLayer?: Phaser.Tilemaps.TilemapLayer | null; + bridgeLayer?: Phaser.Tilemaps.TilemapLayer | null; + dirtLayer?: Phaser.Tilemaps.TilemapLayer | null; + basicPlantsLayer?: Phaser.Tilemaps.TilemapLayer | null; + hillsLayer?: Phaser.Tilemaps.TilemapLayer | null; + woodenHouseLayer?: Phaser.Tilemaps.TilemapLayer | null; + basicGrassLayer?: Phaser.Tilemaps.TilemapLayer | null; + cowLayer?: Phaser.Tilemaps.TilemapLayer | null; + fenceLayer?: Phaser.Tilemaps.TilemapLayer | null; + eggsLayer?: Phaser.Tilemaps.TilemapLayer | null; + chickenLayer?: Phaser.Tilemaps.TilemapLayer | null; + furnitureLayer?: Phaser.Tilemaps.TilemapLayer | null; + wallsLayer?: Phaser.Tilemaps.TilemapLayer | null; + hillsCollidesLayer?: Phaser.Tilemaps.TilemapLayer | null; +} diff --git a/front/src/types/server.ts b/front/src/types/server.ts index e198f489..a6e07bdd 100644 --- a/front/src/types/server.ts +++ b/front/src/types/server.ts @@ -1,29 +1,14 @@ -export interface ServerMapTypes { - server: Phaser.Game | null; - scene: Phaser.Scene | null; +export interface ServerType { + serverId: string; + serverName: string; + image: string; + members: string[]; } -// 현재 활성화된 씬을 부모 컴포넌트로 전달하는 콜백 함수 -export interface ServerMapProps { - currentActiveScene?: (scene_instance: Phaser.Scene) => void; +export interface ServerCardProps { + server: ServerType; } -// Tiled Map 레이어 타입 -export interface Layers { - mapLayer?: Phaser.Tilemaps.TilemapLayer | null; - groundLayer?: Phaser.Tilemaps.TilemapLayer | null; - chickHouseLayer?: Phaser.Tilemaps.TilemapLayer | null; - bridgeLayer?: Phaser.Tilemaps.TilemapLayer | null; - dirtLayer?: Phaser.Tilemaps.TilemapLayer | null; - basicPlantsLayer?: Phaser.Tilemaps.TilemapLayer | null; - hillsLayer?: Phaser.Tilemaps.TilemapLayer | null; - woodenHouseLayer?: Phaser.Tilemaps.TilemapLayer | null; - basicGrassLayer?: Phaser.Tilemaps.TilemapLayer | null; - cowLayer?: Phaser.Tilemaps.TilemapLayer | null; - fenceLayer?: Phaser.Tilemaps.TilemapLayer | null; - eggsLayer?: Phaser.Tilemaps.TilemapLayer | null; - chickenLayer?: Phaser.Tilemaps.TilemapLayer | null; - furnitureLayer?: Phaser.Tilemaps.TilemapLayer | null; - wallsLayer?: Phaser.Tilemaps.TilemapLayer | null; - hillsCollidesLayer?: Phaser.Tilemaps.TilemapLayer | null; +export interface ServerCardListProps { + servers: ServerType[]; } From a53ce3d175ff474ed14dd23e0c15d143e0ea4a9f Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Fri, 7 Jun 2024 18:45:23 +0900 Subject: [PATCH 054/178] =?UTF-8?q?[feat]=20=EC=99=BC=EC=AA=BD=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=20=EC=9C=A0=EC=A0=80=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Layout/Header.tsx | 8 ++-- front/src/components/Layout/Layout.tsx | 2 +- front/src/components/Layout/LeftSideBar.tsx | 13 ++++-- .../components/Layout/ServerInfoSidebar.tsx | 43 ++++++++++++++----- front/src/types/layout.ts | 7 +++ 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/front/src/components/Layout/Header.tsx b/front/src/components/Layout/Header.tsx index ed74d0f3..211e4e9c 100644 --- a/front/src/components/Layout/Header.tsx +++ b/front/src/components/Layout/Header.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; +import MuiAppBar from "@mui/material/AppBar"; import { Box, Drawer, Toolbar, Typography, styled } from "@mui/material"; import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle"; import ChatBubbleIcon from "@mui/icons-material/ChatBubble"; @@ -7,10 +7,7 @@ import FriendsRightSideBar from "./FriendsRightSideBar"; import MessageRightSideBar from "./MessageRightSideBar"; import ChatRoomSideBar from "../Chat/ChatRoomSideBar"; import useChatRoomStore from "../../store/useChatRoomStore"; - -interface AppBarProps extends MuiAppBarProps { - open?: boolean; -} +import { AppBarProps } from "../../types/layout"; const Header = () => { const DRAWER_WIDTH = 240; // 오른쪽 사이드바 넓이 @@ -34,6 +31,7 @@ const Header = () => { const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => prop !== "open", })(({ theme, open }) => ({ + backgroundColor: "#2A2F4F", transition: theme.transitions.create(["margin", "width"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, diff --git a/front/src/components/Layout/Layout.tsx b/front/src/components/Layout/Layout.tsx index 8107da35..74217641 100644 --- a/front/src/components/Layout/Layout.tsx +++ b/front/src/components/Layout/Layout.tsx @@ -3,12 +3,12 @@ import { Outlet } from "react-router-dom"; import theme from "../../theme/themeConfig"; import Header from "./Header"; import LeftSideBar from "./LeftSideBar"; + const Layout: React.FC = () => { return (
-
diff --git a/front/src/components/Layout/LeftSideBar.tsx b/front/src/components/Layout/LeftSideBar.tsx index 46ec0123..048e7922 100644 --- a/front/src/components/Layout/LeftSideBar.tsx +++ b/front/src/components/Layout/LeftSideBar.tsx @@ -47,10 +47,15 @@ const LeftSideBar = () => { }, }}> - - - - + + + diff --git a/front/src/components/Layout/ServerInfoSidebar.tsx b/front/src/components/Layout/ServerInfoSidebar.tsx index 52bee367..6320cfca 100644 --- a/front/src/components/Layout/ServerInfoSidebar.tsx +++ b/front/src/components/Layout/ServerInfoSidebar.tsx @@ -11,27 +11,26 @@ import { Button, styled, Modal, + Box, + Typography, } from "@mui/material"; +import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; import { serverData } from "../../utils/fakeData"; -import MemberProfile from "../Profile/Profile"; -import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import { useNavigate } from "react-router-dom"; import FavoriteStar from "../Home/FavoriteStar"; import ModalComponent from "../Modal/ModalComponent"; import useModal from "../../hooks/Modal"; import Profile from "../Profile/Profile"; -const ServerInfoSidebar: React.FC = ({ - close, - // open, - serverID, -}) => { +const ServerInfoSidebar: React.FC = ({ close, serverID }) => { const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", alignItems: "center", padding: theme.spacing(0, 1), ...theme.mixins.toolbar, justifyContent: "flex-start", + flexDirection: "column", + width: "240px", })); const navigate = useNavigate(); const { isOpen, openModal, closeModal } = useModal(); @@ -39,10 +38,32 @@ const ServerInfoSidebar: React.FC = ({ return ( <> - -
서버 멤버
- - + + + 서버이름 + + + +
diff --git a/front/src/types/layout.ts b/front/src/types/layout.ts index 07d979ce..038b1a61 100644 --- a/front/src/types/layout.ts +++ b/front/src/types/layout.ts @@ -1,3 +1,10 @@ +// Header + +import { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; +export interface AppBarProps extends MuiAppBarProps { + open?: boolean; +} + // 오른쪽 사이드바 export interface SideBarProps { close: () => void; From c14cc82cd15b23fede7d4d4fc9ac3af0f4dc6d94 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Fri, 7 Jun 2024 21:04:37 +0900 Subject: [PATCH 055/178] =?UTF-8?q?bug=20:=20mongodb=20username=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 72e7bbe6..11b41545 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -7,13 +7,13 @@ spring: mongodb: host: ${MONGO_HOST:localhost} port: ${MONGO_PORT:27017} - username: ${MONGO_USER:root} + username: ${MONGO_USERNAME:root} password: ${MONGO_PASSWORD:test1234@} database: ${MONGO_DATABASE:mongodb} authentication-database: admin auth: - url: ${AUTH_SERVICE_URL:http://localhost:8080} + url: ${AUTH_SERVICE_URL:http://localhost/auth} resource: default: From cd456390e86e7ec228feba580dad65d8ba0e4dc5 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Fri, 7 Jun 2024 21:09:07 +0900 Subject: [PATCH 056/178] =?UTF-8?q?bug=20:=20server=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20serverProfileEntity=20id=EA=B0=80=20"test"=EB=A1=9C?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=EB=90=98=EB=8A=94=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/output/mongo/entity/ServerProfileEntity.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt index 4cae2378..b9087519 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt @@ -6,11 +6,13 @@ import org.springframework.data.mongodb.core.mapping.Document @Document("server_profile") class ServerProfileEntity( - @Id val userId: String, val name: String, val imagePath: String, val serverId: String, val role: ServerRole, val bookmarked: Boolean, -) +) { + @Id + private var id: String? = null +} From 32571bd4c0543cc0c8230def9cfbb1c061017911 Mon Sep 17 00:00:00 2001 From: "BOOK-7HF2F58CR4\\sour1" Date: Fri, 7 Jun 2024 23:20:33 +0900 Subject: [PATCH 057/178] =?UTF-8?q?feat=20:=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20WebRtc=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20UI=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/model/Chat.kt | 21 ++ .../chat/chat/repository/ChatRepository.kt | 15 + .../kpring/chat/global/config/ClientConfig.kt | 26 ++ front/src/components/Layout/Header.tsx | 2 +- front/src/components/Map/ServerMap.tsx | 3 + front/src/components/Phaser/EventBus.ts | 5 + front/src/components/Phaser/ServerMap.tsx | 106 +++++++ front/src/components/Phaser/main.tsx | 24 ++ .../components/Phaser/serverScene/MainMap.ts | 284 ++++++++++++++++++ .../Phaser/serverScene/Preloader.ts | 38 +++ .../components/VideoCall/VideoCallBoxList.tsx | 71 +++++ .../VideoCall/VideoCallBoxListItem.tsx | 25 ++ front/tailwind.config.js | 21 +- 13 files changed, 630 insertions(+), 11 deletions(-) create mode 100644 chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt create mode 100644 chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt create mode 100644 chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt create mode 100644 front/src/components/Phaser/EventBus.ts create mode 100644 front/src/components/Phaser/ServerMap.tsx create mode 100644 front/src/components/Phaser/main.tsx create mode 100644 front/src/components/Phaser/serverScene/MainMap.ts create mode 100644 front/src/components/Phaser/serverScene/Preloader.ts create mode 100644 front/src/components/VideoCall/VideoCallBoxList.tsx create mode 100644 front/src/components/VideoCall/VideoCallBoxListItem.tsx diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt new file mode 100644 index 00000000..30f2f80c --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt @@ -0,0 +1,21 @@ +package kpring.chat.chat.model + +import kpring.chat.NoArg +import kpring.chat.global.model.BaseTime +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document + +@NoArg +@Document(collection = "chats") +class Chat( + val userId: String, + val roomId: String, + val content: String, +) : BaseTime() { + @Id + var id: String? = null + + fun isEdited(): Boolean { + return !createdAt.equals(updatedAt) + } +} diff --git a/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt b/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt new file mode 100644 index 00000000..e6413edb --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt @@ -0,0 +1,15 @@ +package kpring.chat.chat.repository + +import kpring.chat.chat.model.Chat +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.data.querydsl.QuerydslPredicateExecutor +import org.springframework.stereotype.Repository + +@Repository +interface ChatRepository : MongoRepository, QuerydslPredicateExecutor { + fun findAllByRoomId( + roomId: String, + pageable: Pageable, + ): List +} diff --git a/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt b/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt new file mode 100644 index 00000000..3e7ebe65 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt @@ -0,0 +1,26 @@ +package kpring.chat.global.config + +import kpring.core.auth.client.AuthClient +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestClient +import org.springframework.web.client.support.RestClientAdapter +import org.springframework.web.service.invoker.HttpServiceProxyFactory + +@Configuration +class ClientConfig { + @Value("\${auth.url}") + private val authUrl: String? = null + + @Bean + fun authClient(): AuthClient { + val restClient = + RestClient.builder() + .baseUrl(authUrl!!) + .build() + val adapter = RestClientAdapter.create(restClient) + val factory = HttpServiceProxyFactory.builderFor(adapter).build() + return factory.createClient(AuthClient::class.java) + } +} diff --git a/front/src/components/Layout/Header.tsx b/front/src/components/Layout/Header.tsx index ed74d0f3..2f7b5c3a 100644 --- a/front/src/components/Layout/Header.tsx +++ b/front/src/components/Layout/Header.tsx @@ -108,4 +108,4 @@ const Header = () => { ); }; -export default Header; +export default Header; \ No newline at end of file diff --git a/front/src/components/Map/ServerMap.tsx b/front/src/components/Map/ServerMap.tsx index bc1940b8..2e252e59 100644 --- a/front/src/components/Map/ServerMap.tsx +++ b/front/src/components/Map/ServerMap.tsx @@ -2,6 +2,7 @@ import { forwardRef, useEffect, useLayoutEffect, useRef } from "react"; import { EventBus } from "./EventBus"; import { ServerMapProps, ServerMapTypes } from "../../types/server"; import EnterServer from "./main"; +import VideoCallBoxList from "../VideoCall/VideoCallBoxList"; // 서버를 생성하고 관리하는 컴포넌트 // forwardRef를 사용해 부모 컴포넌트로부터 ref를 전달 받음 @@ -99,7 +100,9 @@ export const ServerMap = forwardRef(
드래그
+
+ ); } diff --git a/front/src/components/Phaser/EventBus.ts b/front/src/components/Phaser/EventBus.ts new file mode 100644 index 00000000..c60632f4 --- /dev/null +++ b/front/src/components/Phaser/EventBus.ts @@ -0,0 +1,5 @@ +import { Events } from "phaser"; + +// Used to emit events between React components and Phaser scenes +// https://newdocs.phaser.io/docs/3.70.0/Phaser.Events.EventEmitter +export const EventBus = new Events.EventEmitter(); diff --git a/front/src/components/Phaser/ServerMap.tsx b/front/src/components/Phaser/ServerMap.tsx new file mode 100644 index 00000000..bc1940b8 --- /dev/null +++ b/front/src/components/Phaser/ServerMap.tsx @@ -0,0 +1,106 @@ +import { forwardRef, useEffect, useLayoutEffect, useRef } from "react"; +import { EventBus } from "./EventBus"; +import { ServerMapProps, ServerMapTypes } from "../../types/server"; +import EnterServer from "./main"; + +// 서버를 생성하고 관리하는 컴포넌트 +// forwardRef를 사용해 부모 컴포넌트로부터 ref를 전달 받음 +export const ServerMap = forwardRef( + function ServerMap({ currentActiveScene }, ref) { + // Phaser.Game 인스턴스를 저장하기 위한 ref 생성 + const mapRef = useRef(null!); + + // 브라우저 화면 크기에 따라서 맵의 크기도 리사이즈 되는 함수 + const resizeMap = () => { + if (mapRef.current) { + const width = window.innerWidth; + const height = window.innerHeight; + mapRef.current.scale.resize(width, height); + } + }; + + // DOM이 변경된 후, 브라우저 화면 재렌더링 + useLayoutEffect(() => { + // serverRef.current가 null인 경우, EnterServer 함수를 호출하여 서버 초기화 + if (mapRef.current === null) { + mapRef.current = EnterServer("map-container"); + + // 'ref'는 함수로 받거나 객체로 받을 수 있음 + if (typeof ref === "function") { + // 'ref'가 함수로 전달 되는 경우 + ref({ server: mapRef.current, scene: null }); + } else if (ref) { + // 'ref'가 객체로 전달되는 경우 + ref.current = { server: mapRef.current, scene: null }; + } + + resizeMap(); + window.addEventListener("resize", resizeMap, false); + + // 확대 & 축소 이벤트 + document.getElementById("zoom-in")?.addEventListener("click", () => { + if (mapRef.current) { + const mainCamera = mapRef.current.scene.scenes[1].cameras.main; + if (mainCamera) { + mainCamera.setZoom(mainCamera.zoom + 0.05); + } + } + }); + + document.getElementById("zoom-out")?.addEventListener("click", () => { + if (mapRef.current) { + const mainCamera = mapRef.current.scene.scenes[1].cameras.main; + if (mainCamera) { + mainCamera.setZoom(mainCamera.zoom - 0.05); + } + } + }); + } + + // 컴포넌트가 언마운트될 실행되는 함수 + return () => { + if (mapRef.current) { + mapRef.current.destroy(true); + if (mapRef.current !== null) { + mapRef.current = null; + } + } + window.removeEventListener("resize", resizeMap); + }; + }, [ref]); + + // 'currentActiveScene'과 'ref'가 변경될 때 마다 실행 + useEffect(() => { + EventBus.on("current-scene-ready", (scene_instance: Phaser.Scene) => { + if (currentActiveScene && typeof currentActiveScene === "function") { + currentActiveScene(scene_instance); + } + + if (typeof ref === "function") { + ref({ server: mapRef.current, scene: scene_instance }); + } else if (ref) { + ref.current = { server: mapRef.current, scene: scene_instance }; + } + }); + return () => { + EventBus.removeListener("current-scene-ready"); + }; + }, [currentActiveScene, ref]); + + return ( +
+
+
+ 확대 +
+
+ 축소 +
+
+ 드래그 +
+
+
+ ); + } +); diff --git a/front/src/components/Phaser/main.tsx b/front/src/components/Phaser/main.tsx new file mode 100644 index 00000000..e5485648 --- /dev/null +++ b/front/src/components/Phaser/main.tsx @@ -0,0 +1,24 @@ +import { Game } from "phaser"; +import { Preloader } from "./serverScene/Preloader"; +import { MainMap } from "./serverScene/MainMap"; + +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, // Phaser가 웹GL 또는 캔버스를 자동으로 선택해서 렌더링 + scene: [Preloader, MainMap], // 사용할 씬들을 배열로 지정 + physics: { + // 물리 엔진 설정 + default: "arcade", // 충돌 감지와 기본적인 물리 효과 제공 + arcade: { + debug: false, // 디버그모드 활성화 시 충돌 영역과 물리 효과 확인하고 조정가능 + }, + }, +}; + +const EnterServer = (parent: string) => { + const map = new Game({ ...config, parent }); // config 객체에 정의된 설정과 parent를 합쳐 새로운 인스턴스 생성 + map.scene.start("MainMap", { serverInstance: map }); // MainMap를 시작하고 serverInstance라는 이름으로 현재 서버 인스턴스 전달 + + return map; // 생성된 인스턴스를 반환 +}; + +export default EnterServer; diff --git a/front/src/components/Phaser/serverScene/MainMap.ts b/front/src/components/Phaser/serverScene/MainMap.ts new file mode 100644 index 00000000..de4269e0 --- /dev/null +++ b/front/src/components/Phaser/serverScene/MainMap.ts @@ -0,0 +1,284 @@ +import { Scene } from "phaser"; +import { Layers } from "../../../types/server"; + +export class MainMap extends Scene { + private mapInstance!: Phaser.Game; + private character!: Phaser.Physics.Arcade.Sprite; + private keyboards!: Phaser.Types.Input.Keyboard.CursorKeys; + private isDragging: boolean = false; + private dragStartX: number = 0; + private dragStartY: number = 0; + + constructor() { + super("MainMap"); + } + + init(data: { mapInstance: Phaser.Game }) { + this.mapInstance = data.mapInstance; + } + + create() { + // 3. preload에서 tilemapTiledJSON에서 지정한 string key와 매치시켜 map으로 지정 + const map = this.make.tilemap({ key: "firstMap" }); + + // 4. Tiled에서 그린 잔디, 집, 나무 등과 같은 타일 요소들을 화면에 뿌려준다. + // 첫번째 param: Tiled에서 지정한 tilessets의 이름 + // 두번째 param: preload에서 지정한 이미지 파일 key + const hillsLayerTileset = map.addTilesetImage("hills", "hillImg"); + const dirtLayerTileset = map.addTilesetImage("dirt", "dirtImg"); + const basicPlantsTileset = map.addTilesetImage( + "basic_plants", + "basicPlantsImg" + ); + const grassTileset = map.addTilesetImage("grass", "grassImg"); + const waterTileset = map.addTilesetImage("water", "waterImg"); + const chickHouseTileset = map.addTilesetImage( + "chick_house", + "chickHouseImg" + ); + const basicGrassTileset = map.addTilesetImage("basic_grass", "treeImg"); + const bridgeTileset = map.addTilesetImage("bridge", "bridgeImg"); + const woodenHouseTileset = map.addTilesetImage( + "wooden_house", + "woodenHouseImg" + ); + const cowTileset = map.addTilesetImage("cow", "cowImg"); + const fenceTileset = map.addTilesetImage("fence", "fenceImg"); + const eggsTileset = map.addTilesetImage("eggs", "eggsImg"); + const chickenTileset = map.addTilesetImage("chicken", "chickenImg"); + const furnitureTilset = map.addTilesetImage("furniture", "furnitureImg"); + + // 5. Tiled에서 지정한 Layer이름으로 해당 layer를 화면에 랜더링 + let layers: Layers = {}; + if ( + grassTileset && + waterTileset && + chickHouseTileset && + basicGrassTileset && + bridgeTileset && + dirtLayerTileset && + basicPlantsTileset && + hillsLayerTileset && + woodenHouseTileset && + cowTileset && + fenceTileset && + eggsTileset && + chickenTileset && + furnitureTilset + ) { + layers.mapLayer = map.createLayer("map", waterTileset); + layers.groundLayer = map.createLayer("grass", grassTileset); + layers.fenceLayer = map.createLayer("fence", fenceTileset); + layers.chickHouseLayer = map.createLayer( + "chick_house", + chickHouseTileset + ); + layers.bridgeLayer = map.createLayer("bridge", bridgeTileset); + layers.dirtLayer = map.createLayer("dirt", dirtLayerTileset); + layers.basicPlantsLayer = map.createLayer( + "basic_plants", + basicPlantsTileset + ); + layers.hillsLayer = map.createLayer("hills", hillsLayerTileset); + layers.woodenHouseLayer = map.createLayer("house", woodenHouseTileset); + layers.basicGrassLayer = map.createLayer( + "basic_grass_1", + basicGrassTileset + ); + layers.cowLayer = map.createLayer("cow", cowTileset); + layers.eggsLayer = map.createLayer("eggs", eggsTileset); + layers.chickenLayer = map.createLayer("chicken", chickenTileset); + layers.furnitureLayer = map.createLayer("furniture", furnitureTilset); + } + + // Collides Debug + layers.hillsLayer?.setCollisionByProperty({ collides: true }); + layers.groundLayer?.setCollisionByProperty({ collides: true }); + layers.fenceLayer?.setCollisionByProperty({ collides: true }); + layers.furnitureLayer?.setCollisionByProperty({ collides: true }); + layers.woodenHouseLayer?.setCollisionByProperty({ collides: true }); + + const debugGraphics = this.add.graphics().setAlpha(0.7); + layers.hillsLayer?.renderDebug(debugGraphics, { + tileColor: null, + collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), + faceColor: new Phaser.Display.Color(40, 39, 37, 255), + }); + + layers.groundLayer?.renderDebug(debugGraphics, { + tileColor: null, + collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), + faceColor: new Phaser.Display.Color(40, 39, 37, 255), + }); + + layers.fenceLayer?.renderDebug(debugGraphics, { + tileColor: null, + collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), + faceColor: new Phaser.Display.Color(40, 39, 37, 255), + }); + layers.furnitureLayer?.renderDebug(debugGraphics, { + tileColor: null, + collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), + faceColor: new Phaser.Display.Color(40, 39, 37, 255), + }); + layers.woodenHouseLayer?.renderDebug(debugGraphics, { + tileColor: null, + collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), + faceColor: new Phaser.Display.Color(40, 39, 37, 255), + }); + + // 텍스처(캐릭터 이미지) 로드가 완료되었는지 확인 + this.load.on("complete", () => { + if (this.textures.exists("basic_character")) { + // 텍스처 로드가 완료되면 캐릭터 생성 + this.character = this.physics.add.sprite( + 300, + 300, + "basic_character", // preload파일에서 atlas의 key값과 동일한 key값 + "move-down-3.png" // 움직이지 않는 상태의 기본 캐릭터 + ); + + // Move-Down + this.anims.create({ + key: "basic_character_move_down", + frames: this.anims.generateFrameNames("basic_character", { + start: 1, + end: 4, + prefix: "move-down-", + suffix: ".png", + }), + frameRate: 15, + repeat: -1, + }); + + // Move-Up + this.anims.create({ + key: "basic_character_move_up", + frames: this.anims.generateFrameNames("basic_character", { + start: 1, + end: 4, + prefix: "move-up-", + suffix: ".png", + }), + frameRate: 15, + repeat: -1, + }); + + // Move-Left + this.anims.create({ + key: "basic_character_move_left", + frames: this.anims.generateFrameNames("basic_character", { + start: 1, + end: 4, + prefix: "move-left-", + suffix: ".png", + }), + frameRate: 15, + repeat: -1, + }); + + // Move-Right + this.anims.create({ + key: "basic_character_move_right", + frames: this.anims.generateFrameNames("basic_character", { + start: 1, + end: 4, + prefix: "move-right-", + suffix: ".png", + }), + frameRate: 15, + repeat: -1, + }); + + this.physics.add.collider( + this.character, + layers.hillsLayer as Phaser.Tilemaps.TilemapLayer + ); + + this.keyboards = this.input.keyboard?.createCursorKeys()!; + this.cameras.main.startFollow(this.character); // 캐릭터가 움직이는 방향으로 카메라도 함께 이동 + } else { + console.log("캐릭터 로드 실패!"); + } + }); + + this.load.start(); + + // 초기 랜더링 맵 크기 지정 + const mapWidth = map.widthInPixels; + const mapHeight = map.heightInPixels; + + this.cameras.main.setBounds(0, 0, mapWidth, mapHeight); + this.physics.world.setBounds(0, 0, mapWidth, mapHeight); + + const dragElement = document.getElementById("drag"); + + if (dragElement) { + dragElement.addEventListener("mousedown", this.onDragStart.bind(this)); + dragElement.addEventListener("mousemove", this.onDragMove.bind(this)); + dragElement.addEventListener("mouseup", this.onDragEnd.bind(this)); + dragElement.addEventListener("mouseleave", this.onDragEnd.bind(this)); + } + + return layers; + } + + onDragStart(this: MainMap, event: MouseEvent) { + this.isDragging = true; + this.dragStartX = event.clientX; + this.dragStartY = event.clientY; + + const dragElement = event.target as HTMLElement; + dragElement.classList.add("dragging"); + } + + onDragMove(this: MainMap, event: MouseEvent) { + if (this.isDragging) { + const deltaX = this.dragStartX - event.clientX; + const deltaY = this.dragStartY - event.clientY; + + if (this.cameras.main) { + console.log(deltaX, deltaY); + this.cameras.main.scrollX += deltaX; + this.cameras.main.scrollY += deltaY; + } + + this.dragStartX = event.clientX; + this.dragStartY = event.clientY; + } + } + + onDragEnd(this: MainMap, event: MouseEvent) { + this.isDragging = false; + const dragElement = event.target as HTMLElement; + dragElement.classList.remove("dragging"); + } + + update() { + if (!this.character) { + // 만약 캐리터가 로드되지 않았다면 아무것도 반환하지 않는다 + return; + } + + this.character.setVelocity(0); + + if (this.keyboards.down.isDown) { + this.character.setVelocityY(100); + this.character.anims.play("basic_character_move_down", true); + } + if (this.keyboards.up.isDown) { + this.character.setVelocityY(-100); + this.character.anims.play("basic_character_move_up", true); + } + if (this.keyboards.left.isDown) { + this.character.setVelocityX(-100); + this.character.anims.play("basic_character_move_left", true); + } + if (this.keyboards.right.isDown) { + this.character.setVelocityX(100); + this.character.anims.play("basic_character_move_right", true); + } else { + this.character.anims.stop(); + } + } +} diff --git a/front/src/components/Phaser/serverScene/Preloader.ts b/front/src/components/Phaser/serverScene/Preloader.ts new file mode 100644 index 00000000..7336b661 --- /dev/null +++ b/front/src/components/Phaser/serverScene/Preloader.ts @@ -0,0 +1,38 @@ +import { Scene } from "phaser"; + +export class Preloader extends Scene { + constructor() { + super("Preloader"); + } + + preload() { + // 1. assets폴더에서 사용할 png파일을 모두 불러온다. + this.load.image("grassImg", "../assets/grass.png"); + this.load.image("waterImg", "../assets/water.png"); + this.load.image("bridgeImg", "../assets/bridge.png"); + this.load.image("chickHouseImg", "../assets/chick_house.png"); + this.load.image("treeImg", "../assets/trees.png"); + this.load.image("dirtImg", "../assets/dirt.png"); + this.load.image("basicPlantsImg", "../assets/basic_plants.png"); + this.load.image("hillImg", "../assets/hills.png"); + this.load.image("woodenHouseImg", "../assets/wooden_house.png"); + this.load.image("basicGrassImg", "../assets/basic_grass.png"); + this.load.image("cowImg", "../assets/cow.png"); + this.load.image("fenceImg", "../assets/fences.png"); + this.load.image("chickenImg", "../assets/chicken.png"); + this.load.image("eggsImg", "../assets/eggs.png"); + this.load.image("furnitureImg", "../assets/furniture.png"); + this.load.atlas( + "basic_character", + "../assets/character.png", + "../assets/character.json" + ); + + // 2. Tiled에서 작업하고 JSON파일 저장한 후 불러온다. + this.load.tilemapTiledJSON("firstMap", "../assets/firstMap.json"); + } + + create() { + this.scene.start("MainMap"); + } +} diff --git a/front/src/components/VideoCall/VideoCallBoxList.tsx b/front/src/components/VideoCall/VideoCallBoxList.tsx new file mode 100644 index 00000000..6ce26c57 --- /dev/null +++ b/front/src/components/VideoCall/VideoCallBoxList.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import VideoCallBoxListItem from './VideoCallBoxListItem' +import { messageMemberList } from "../../utils/fakeData"; +import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import { Member } from '../../types/layout'; + +const VideoCallBoxList = () => { + const [curVideoCallBoxPage, setCurVideoCallBoxPage] = useState(0); + const [slicedMemberList, setSlicedMemberList] = useState([]); // 페이징 처리 된 멤버 리스트 + + // 마지막 페이지 수 + const lastPage = useMemo(()=>{ + const memberCnt = messageMemberList.length; + let lastPage = 0; + if(memberCnt%4 === 0){ + lastPage = Math.floor(memberCnt/4) - 1 + }else{ + lastPage = Math.floor(memberCnt/4) + } + return lastPage + },[]) + + // TODO : 화면 공유 박스 이전 페이지 이동 핸들링 함수 + const handleBoxPagePrev = useCallback(()=>{ + let curPage = curVideoCallBoxPage; + if(curPage!==0){ + setCurVideoCallBoxPage(curPage - 1) + } + },[curVideoCallBoxPage]) + + // // TODO : 화면 공유 박스 다음 페이지 이동 핸들링 함수 + const handleBoxPageNext = useCallback(()=>{ + if(curVideoCallBoxPage!==lastPage){ + let curPage = curVideoCallBoxPage; + setCurVideoCallBoxPage(curPage + 1) + } + },[curVideoCallBoxPage,lastPage]) + + + // TODO : 화면공유 멤버 리스트 슬라이싱 함수 + const sliceMemberList = useCallback(()=>{ + const newMemberList = messageMemberList.slice(curVideoCallBoxPage*4, (curVideoCallBoxPage*4)+4); + setSlicedMemberList(newMemberList) + },[curVideoCallBoxPage]) + + useEffect(() => { + sliceMemberList(); + }, [sliceMemberList]); + + return ( +
+ +
+ { + slicedMemberList.map(member=>( + + )) + } +
+ +
+ ) +} + +export default VideoCallBoxList \ No newline at end of file diff --git a/front/src/components/VideoCall/VideoCallBoxListItem.tsx b/front/src/components/VideoCall/VideoCallBoxListItem.tsx new file mode 100644 index 00000000..852cd279 --- /dev/null +++ b/front/src/components/VideoCall/VideoCallBoxListItem.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { Member } from '../../types/layout' +import { Avatar } from '@mui/material'; + +interface MemberListItemProps{ + member: Member; +} + +const VideoCallBoxListItem : React.FC= ({member}) => { + return ( +
+
+ +
+
{member.userName}
+
+ +
+ + +
+ ) +} + +export default VideoCallBoxListItem \ No newline at end of file diff --git a/front/tailwind.config.js b/front/tailwind.config.js index 4a85e8bb..55353a6d 100644 --- a/front/tailwind.config.js +++ b/front/tailwind.config.js @@ -2,17 +2,18 @@ module.exports = { content: ["./src/**/*.{html,js,jsx,ts,tsx}"], theme: { - extend: {}, - colors: { - dark:{ - DEFAULT : "#2A2F4F", + extend: { + colors: { + dark:{ + DEFAULT : "#2A2F4F", + }, + pink:{ + DEFAULT : "#FDE2F3", + }, + darkPink:{ + DEFAULT : "#E5BEEC", + } }, - pink:{ - DEFAULT : "#FDE2F3", - }, - darkPink:{ - DEFAULT : "#E5BEEC", - } }, }, plugins: [], From 270e0618255739f77b53fa733afce8cde220ddf2 Mon Sep 17 00:00:00 2001 From: "BOOK-7HF2F58CR4\\sour1" Date: Sat, 8 Jun 2024 10:25:16 +0900 Subject: [PATCH 058/178] =?UTF-8?q?remove=20:=20=EA=B9=83=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A1=9C=20=EC=97=86=EC=96=B4=EC=A0=B8=EC=95=BC=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=8C=8C=EC=9D=BC=EC=9D=B4=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=B4=EC=84=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/model/Chat.kt | 21 -- .../chat/chat/repository/ChatRepository.kt | 15 - .../kpring/chat/global/config/ClientConfig.kt | 26 -- front/src/components/Phaser/EventBus.ts | 5 - front/src/components/Phaser/ServerMap.tsx | 106 ------- front/src/components/Phaser/main.tsx | 24 -- .../components/Phaser/serverScene/MainMap.ts | 284 ------------------ .../Phaser/serverScene/Preloader.ts | 38 --- front/tailwind.config.js | 1 + 9 files changed, 1 insertion(+), 519 deletions(-) delete mode 100644 chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt delete mode 100644 chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt delete mode 100644 chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt delete mode 100644 front/src/components/Phaser/EventBus.ts delete mode 100644 front/src/components/Phaser/ServerMap.tsx delete mode 100644 front/src/components/Phaser/main.tsx delete mode 100644 front/src/components/Phaser/serverScene/MainMap.ts delete mode 100644 front/src/components/Phaser/serverScene/Preloader.ts diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt deleted file mode 100644 index 30f2f80c..00000000 --- a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt +++ /dev/null @@ -1,21 +0,0 @@ -package kpring.chat.chat.model - -import kpring.chat.NoArg -import kpring.chat.global.model.BaseTime -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document - -@NoArg -@Document(collection = "chats") -class Chat( - val userId: String, - val roomId: String, - val content: String, -) : BaseTime() { - @Id - var id: String? = null - - fun isEdited(): Boolean { - return !createdAt.equals(updatedAt) - } -} diff --git a/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt b/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt deleted file mode 100644 index e6413edb..00000000 --- a/chat/src/main/kotlin/kpring/chat/chat/repository/ChatRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package kpring.chat.chat.repository - -import kpring.chat.chat.model.Chat -import org.springframework.data.domain.Pageable -import org.springframework.data.mongodb.repository.MongoRepository -import org.springframework.data.querydsl.QuerydslPredicateExecutor -import org.springframework.stereotype.Repository - -@Repository -interface ChatRepository : MongoRepository, QuerydslPredicateExecutor { - fun findAllByRoomId( - roomId: String, - pageable: Pageable, - ): List -} diff --git a/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt b/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt deleted file mode 100644 index 3e7ebe65..00000000 --- a/chat/src/main/kotlin/kpring/chat/global/config/ClientConfig.kt +++ /dev/null @@ -1,26 +0,0 @@ -package kpring.chat.global.config - -import kpring.core.auth.client.AuthClient -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.web.client.RestClient -import org.springframework.web.client.support.RestClientAdapter -import org.springframework.web.service.invoker.HttpServiceProxyFactory - -@Configuration -class ClientConfig { - @Value("\${auth.url}") - private val authUrl: String? = null - - @Bean - fun authClient(): AuthClient { - val restClient = - RestClient.builder() - .baseUrl(authUrl!!) - .build() - val adapter = RestClientAdapter.create(restClient) - val factory = HttpServiceProxyFactory.builderFor(adapter).build() - return factory.createClient(AuthClient::class.java) - } -} diff --git a/front/src/components/Phaser/EventBus.ts b/front/src/components/Phaser/EventBus.ts deleted file mode 100644 index c60632f4..00000000 --- a/front/src/components/Phaser/EventBus.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Events } from "phaser"; - -// Used to emit events between React components and Phaser scenes -// https://newdocs.phaser.io/docs/3.70.0/Phaser.Events.EventEmitter -export const EventBus = new Events.EventEmitter(); diff --git a/front/src/components/Phaser/ServerMap.tsx b/front/src/components/Phaser/ServerMap.tsx deleted file mode 100644 index bc1940b8..00000000 --- a/front/src/components/Phaser/ServerMap.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { forwardRef, useEffect, useLayoutEffect, useRef } from "react"; -import { EventBus } from "./EventBus"; -import { ServerMapProps, ServerMapTypes } from "../../types/server"; -import EnterServer from "./main"; - -// 서버를 생성하고 관리하는 컴포넌트 -// forwardRef를 사용해 부모 컴포넌트로부터 ref를 전달 받음 -export const ServerMap = forwardRef( - function ServerMap({ currentActiveScene }, ref) { - // Phaser.Game 인스턴스를 저장하기 위한 ref 생성 - const mapRef = useRef(null!); - - // 브라우저 화면 크기에 따라서 맵의 크기도 리사이즈 되는 함수 - const resizeMap = () => { - if (mapRef.current) { - const width = window.innerWidth; - const height = window.innerHeight; - mapRef.current.scale.resize(width, height); - } - }; - - // DOM이 변경된 후, 브라우저 화면 재렌더링 - useLayoutEffect(() => { - // serverRef.current가 null인 경우, EnterServer 함수를 호출하여 서버 초기화 - if (mapRef.current === null) { - mapRef.current = EnterServer("map-container"); - - // 'ref'는 함수로 받거나 객체로 받을 수 있음 - if (typeof ref === "function") { - // 'ref'가 함수로 전달 되는 경우 - ref({ server: mapRef.current, scene: null }); - } else if (ref) { - // 'ref'가 객체로 전달되는 경우 - ref.current = { server: mapRef.current, scene: null }; - } - - resizeMap(); - window.addEventListener("resize", resizeMap, false); - - // 확대 & 축소 이벤트 - document.getElementById("zoom-in")?.addEventListener("click", () => { - if (mapRef.current) { - const mainCamera = mapRef.current.scene.scenes[1].cameras.main; - if (mainCamera) { - mainCamera.setZoom(mainCamera.zoom + 0.05); - } - } - }); - - document.getElementById("zoom-out")?.addEventListener("click", () => { - if (mapRef.current) { - const mainCamera = mapRef.current.scene.scenes[1].cameras.main; - if (mainCamera) { - mainCamera.setZoom(mainCamera.zoom - 0.05); - } - } - }); - } - - // 컴포넌트가 언마운트될 실행되는 함수 - return () => { - if (mapRef.current) { - mapRef.current.destroy(true); - if (mapRef.current !== null) { - mapRef.current = null; - } - } - window.removeEventListener("resize", resizeMap); - }; - }, [ref]); - - // 'currentActiveScene'과 'ref'가 변경될 때 마다 실행 - useEffect(() => { - EventBus.on("current-scene-ready", (scene_instance: Phaser.Scene) => { - if (currentActiveScene && typeof currentActiveScene === "function") { - currentActiveScene(scene_instance); - } - - if (typeof ref === "function") { - ref({ server: mapRef.current, scene: scene_instance }); - } else if (ref) { - ref.current = { server: mapRef.current, scene: scene_instance }; - } - }); - return () => { - EventBus.removeListener("current-scene-ready"); - }; - }, [currentActiveScene, ref]); - - return ( -
-
-
- 확대 -
-
- 축소 -
-
- 드래그 -
-
-
- ); - } -); diff --git a/front/src/components/Phaser/main.tsx b/front/src/components/Phaser/main.tsx deleted file mode 100644 index e5485648..00000000 --- a/front/src/components/Phaser/main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Game } from "phaser"; -import { Preloader } from "./serverScene/Preloader"; -import { MainMap } from "./serverScene/MainMap"; - -const config: Phaser.Types.Core.GameConfig = { - type: Phaser.AUTO, // Phaser가 웹GL 또는 캔버스를 자동으로 선택해서 렌더링 - scene: [Preloader, MainMap], // 사용할 씬들을 배열로 지정 - physics: { - // 물리 엔진 설정 - default: "arcade", // 충돌 감지와 기본적인 물리 효과 제공 - arcade: { - debug: false, // 디버그모드 활성화 시 충돌 영역과 물리 효과 확인하고 조정가능 - }, - }, -}; - -const EnterServer = (parent: string) => { - const map = new Game({ ...config, parent }); // config 객체에 정의된 설정과 parent를 합쳐 새로운 인스턴스 생성 - map.scene.start("MainMap", { serverInstance: map }); // MainMap를 시작하고 serverInstance라는 이름으로 현재 서버 인스턴스 전달 - - return map; // 생성된 인스턴스를 반환 -}; - -export default EnterServer; diff --git a/front/src/components/Phaser/serverScene/MainMap.ts b/front/src/components/Phaser/serverScene/MainMap.ts deleted file mode 100644 index de4269e0..00000000 --- a/front/src/components/Phaser/serverScene/MainMap.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { Scene } from "phaser"; -import { Layers } from "../../../types/server"; - -export class MainMap extends Scene { - private mapInstance!: Phaser.Game; - private character!: Phaser.Physics.Arcade.Sprite; - private keyboards!: Phaser.Types.Input.Keyboard.CursorKeys; - private isDragging: boolean = false; - private dragStartX: number = 0; - private dragStartY: number = 0; - - constructor() { - super("MainMap"); - } - - init(data: { mapInstance: Phaser.Game }) { - this.mapInstance = data.mapInstance; - } - - create() { - // 3. preload에서 tilemapTiledJSON에서 지정한 string key와 매치시켜 map으로 지정 - const map = this.make.tilemap({ key: "firstMap" }); - - // 4. Tiled에서 그린 잔디, 집, 나무 등과 같은 타일 요소들을 화면에 뿌려준다. - // 첫번째 param: Tiled에서 지정한 tilessets의 이름 - // 두번째 param: preload에서 지정한 이미지 파일 key - const hillsLayerTileset = map.addTilesetImage("hills", "hillImg"); - const dirtLayerTileset = map.addTilesetImage("dirt", "dirtImg"); - const basicPlantsTileset = map.addTilesetImage( - "basic_plants", - "basicPlantsImg" - ); - const grassTileset = map.addTilesetImage("grass", "grassImg"); - const waterTileset = map.addTilesetImage("water", "waterImg"); - const chickHouseTileset = map.addTilesetImage( - "chick_house", - "chickHouseImg" - ); - const basicGrassTileset = map.addTilesetImage("basic_grass", "treeImg"); - const bridgeTileset = map.addTilesetImage("bridge", "bridgeImg"); - const woodenHouseTileset = map.addTilesetImage( - "wooden_house", - "woodenHouseImg" - ); - const cowTileset = map.addTilesetImage("cow", "cowImg"); - const fenceTileset = map.addTilesetImage("fence", "fenceImg"); - const eggsTileset = map.addTilesetImage("eggs", "eggsImg"); - const chickenTileset = map.addTilesetImage("chicken", "chickenImg"); - const furnitureTilset = map.addTilesetImage("furniture", "furnitureImg"); - - // 5. Tiled에서 지정한 Layer이름으로 해당 layer를 화면에 랜더링 - let layers: Layers = {}; - if ( - grassTileset && - waterTileset && - chickHouseTileset && - basicGrassTileset && - bridgeTileset && - dirtLayerTileset && - basicPlantsTileset && - hillsLayerTileset && - woodenHouseTileset && - cowTileset && - fenceTileset && - eggsTileset && - chickenTileset && - furnitureTilset - ) { - layers.mapLayer = map.createLayer("map", waterTileset); - layers.groundLayer = map.createLayer("grass", grassTileset); - layers.fenceLayer = map.createLayer("fence", fenceTileset); - layers.chickHouseLayer = map.createLayer( - "chick_house", - chickHouseTileset - ); - layers.bridgeLayer = map.createLayer("bridge", bridgeTileset); - layers.dirtLayer = map.createLayer("dirt", dirtLayerTileset); - layers.basicPlantsLayer = map.createLayer( - "basic_plants", - basicPlantsTileset - ); - layers.hillsLayer = map.createLayer("hills", hillsLayerTileset); - layers.woodenHouseLayer = map.createLayer("house", woodenHouseTileset); - layers.basicGrassLayer = map.createLayer( - "basic_grass_1", - basicGrassTileset - ); - layers.cowLayer = map.createLayer("cow", cowTileset); - layers.eggsLayer = map.createLayer("eggs", eggsTileset); - layers.chickenLayer = map.createLayer("chicken", chickenTileset); - layers.furnitureLayer = map.createLayer("furniture", furnitureTilset); - } - - // Collides Debug - layers.hillsLayer?.setCollisionByProperty({ collides: true }); - layers.groundLayer?.setCollisionByProperty({ collides: true }); - layers.fenceLayer?.setCollisionByProperty({ collides: true }); - layers.furnitureLayer?.setCollisionByProperty({ collides: true }); - layers.woodenHouseLayer?.setCollisionByProperty({ collides: true }); - - const debugGraphics = this.add.graphics().setAlpha(0.7); - layers.hillsLayer?.renderDebug(debugGraphics, { - tileColor: null, - collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), - faceColor: new Phaser.Display.Color(40, 39, 37, 255), - }); - - layers.groundLayer?.renderDebug(debugGraphics, { - tileColor: null, - collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), - faceColor: new Phaser.Display.Color(40, 39, 37, 255), - }); - - layers.fenceLayer?.renderDebug(debugGraphics, { - tileColor: null, - collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), - faceColor: new Phaser.Display.Color(40, 39, 37, 255), - }); - layers.furnitureLayer?.renderDebug(debugGraphics, { - tileColor: null, - collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), - faceColor: new Phaser.Display.Color(40, 39, 37, 255), - }); - layers.woodenHouseLayer?.renderDebug(debugGraphics, { - tileColor: null, - collidingTileColor: new Phaser.Display.Color(243, 234, 48, 255), - faceColor: new Phaser.Display.Color(40, 39, 37, 255), - }); - - // 텍스처(캐릭터 이미지) 로드가 완료되었는지 확인 - this.load.on("complete", () => { - if (this.textures.exists("basic_character")) { - // 텍스처 로드가 완료되면 캐릭터 생성 - this.character = this.physics.add.sprite( - 300, - 300, - "basic_character", // preload파일에서 atlas의 key값과 동일한 key값 - "move-down-3.png" // 움직이지 않는 상태의 기본 캐릭터 - ); - - // Move-Down - this.anims.create({ - key: "basic_character_move_down", - frames: this.anims.generateFrameNames("basic_character", { - start: 1, - end: 4, - prefix: "move-down-", - suffix: ".png", - }), - frameRate: 15, - repeat: -1, - }); - - // Move-Up - this.anims.create({ - key: "basic_character_move_up", - frames: this.anims.generateFrameNames("basic_character", { - start: 1, - end: 4, - prefix: "move-up-", - suffix: ".png", - }), - frameRate: 15, - repeat: -1, - }); - - // Move-Left - this.anims.create({ - key: "basic_character_move_left", - frames: this.anims.generateFrameNames("basic_character", { - start: 1, - end: 4, - prefix: "move-left-", - suffix: ".png", - }), - frameRate: 15, - repeat: -1, - }); - - // Move-Right - this.anims.create({ - key: "basic_character_move_right", - frames: this.anims.generateFrameNames("basic_character", { - start: 1, - end: 4, - prefix: "move-right-", - suffix: ".png", - }), - frameRate: 15, - repeat: -1, - }); - - this.physics.add.collider( - this.character, - layers.hillsLayer as Phaser.Tilemaps.TilemapLayer - ); - - this.keyboards = this.input.keyboard?.createCursorKeys()!; - this.cameras.main.startFollow(this.character); // 캐릭터가 움직이는 방향으로 카메라도 함께 이동 - } else { - console.log("캐릭터 로드 실패!"); - } - }); - - this.load.start(); - - // 초기 랜더링 맵 크기 지정 - const mapWidth = map.widthInPixels; - const mapHeight = map.heightInPixels; - - this.cameras.main.setBounds(0, 0, mapWidth, mapHeight); - this.physics.world.setBounds(0, 0, mapWidth, mapHeight); - - const dragElement = document.getElementById("drag"); - - if (dragElement) { - dragElement.addEventListener("mousedown", this.onDragStart.bind(this)); - dragElement.addEventListener("mousemove", this.onDragMove.bind(this)); - dragElement.addEventListener("mouseup", this.onDragEnd.bind(this)); - dragElement.addEventListener("mouseleave", this.onDragEnd.bind(this)); - } - - return layers; - } - - onDragStart(this: MainMap, event: MouseEvent) { - this.isDragging = true; - this.dragStartX = event.clientX; - this.dragStartY = event.clientY; - - const dragElement = event.target as HTMLElement; - dragElement.classList.add("dragging"); - } - - onDragMove(this: MainMap, event: MouseEvent) { - if (this.isDragging) { - const deltaX = this.dragStartX - event.clientX; - const deltaY = this.dragStartY - event.clientY; - - if (this.cameras.main) { - console.log(deltaX, deltaY); - this.cameras.main.scrollX += deltaX; - this.cameras.main.scrollY += deltaY; - } - - this.dragStartX = event.clientX; - this.dragStartY = event.clientY; - } - } - - onDragEnd(this: MainMap, event: MouseEvent) { - this.isDragging = false; - const dragElement = event.target as HTMLElement; - dragElement.classList.remove("dragging"); - } - - update() { - if (!this.character) { - // 만약 캐리터가 로드되지 않았다면 아무것도 반환하지 않는다 - return; - } - - this.character.setVelocity(0); - - if (this.keyboards.down.isDown) { - this.character.setVelocityY(100); - this.character.anims.play("basic_character_move_down", true); - } - if (this.keyboards.up.isDown) { - this.character.setVelocityY(-100); - this.character.anims.play("basic_character_move_up", true); - } - if (this.keyboards.left.isDown) { - this.character.setVelocityX(-100); - this.character.anims.play("basic_character_move_left", true); - } - if (this.keyboards.right.isDown) { - this.character.setVelocityX(100); - this.character.anims.play("basic_character_move_right", true); - } else { - this.character.anims.stop(); - } - } -} diff --git a/front/src/components/Phaser/serverScene/Preloader.ts b/front/src/components/Phaser/serverScene/Preloader.ts deleted file mode 100644 index 7336b661..00000000 --- a/front/src/components/Phaser/serverScene/Preloader.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Scene } from "phaser"; - -export class Preloader extends Scene { - constructor() { - super("Preloader"); - } - - preload() { - // 1. assets폴더에서 사용할 png파일을 모두 불러온다. - this.load.image("grassImg", "../assets/grass.png"); - this.load.image("waterImg", "../assets/water.png"); - this.load.image("bridgeImg", "../assets/bridge.png"); - this.load.image("chickHouseImg", "../assets/chick_house.png"); - this.load.image("treeImg", "../assets/trees.png"); - this.load.image("dirtImg", "../assets/dirt.png"); - this.load.image("basicPlantsImg", "../assets/basic_plants.png"); - this.load.image("hillImg", "../assets/hills.png"); - this.load.image("woodenHouseImg", "../assets/wooden_house.png"); - this.load.image("basicGrassImg", "../assets/basic_grass.png"); - this.load.image("cowImg", "../assets/cow.png"); - this.load.image("fenceImg", "../assets/fences.png"); - this.load.image("chickenImg", "../assets/chicken.png"); - this.load.image("eggsImg", "../assets/eggs.png"); - this.load.image("furnitureImg", "../assets/furniture.png"); - this.load.atlas( - "basic_character", - "../assets/character.png", - "../assets/character.json" - ); - - // 2. Tiled에서 작업하고 JSON파일 저장한 후 불러온다. - this.load.tilemapTiledJSON("firstMap", "../assets/firstMap.json"); - } - - create() { - this.scene.start("MainMap"); - } -} diff --git a/front/tailwind.config.js b/front/tailwind.config.js index 55353a6d..79b30684 100644 --- a/front/tailwind.config.js +++ b/front/tailwind.config.js @@ -3,6 +3,7 @@ module.exports = { content: ["./src/**/*.{html,js,jsx,ts,tsx}"], theme: { extend: { + // 기존 색상에서 확장 위해서 extend에 colors 넣는 방식으로 변경 colors: { dark:{ DEFAULT : "#2A2F4F", From 1df0e97abb87b1043c0f5a283190ea463f5f06d5 Mon Sep 17 00:00:00 2001 From: jihyun-j Date: Sat, 8 Jun 2024 12:26:34 +0900 Subject: [PATCH 059/178] =?UTF-8?q?[feat]=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=A7=90=ED=92=8D=EC=84=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Map/ServerMap.tsx | 24 +++++++++++++++++++ front/src/components/Map/ServerMap/MainMap.ts | 17 +++++++++++++ 2 files changed, 41 insertions(+) diff --git a/front/src/components/Map/ServerMap.tsx b/front/src/components/Map/ServerMap.tsx index 45f8c9bb..e68b5905 100644 --- a/front/src/components/Map/ServerMap.tsx +++ b/front/src/components/Map/ServerMap.tsx @@ -2,6 +2,7 @@ import { forwardRef, useEffect, useLayoutEffect, useRef } from "react"; import { EventBus } from "./EventBus"; import { ServerMapProps, ServerMapTypes } from "../../types/map"; import EnterServer from "./main"; +import { MainMap } from "./ServerMap/MainMap"; // 서버를 생성하고 관리하는 컴포넌트 // forwardRef를 사용해 부모 컴포넌트로부터 ref를 전달 받음 @@ -87,9 +88,31 @@ export const ServerMap = forwardRef( }; }, [currentActiveScene, ref]); + useEffect(() => { + const inputField = document.getElementById( + "chat-input" + ) as HTMLInputElement; + console.log(inputField); + const handleKeyEnter = (event: KeyboardEvent) => { + if (event.key === "Enter") { + const target = event.target as HTMLInputElement; + + const mainScene = mapRef.current?.scene.getScene( + "MainMap" + ) as MainMap; + mainScene?.setBalloonText(target.value); + } + }; + inputField?.addEventListener("keydown", handleKeyEnter); + return () => { + inputField?.removeEventListener("keydown", handleKeyEnter); + }; + }, []); + return (
+
확대
@@ -99,6 +122,7 @@ export const ServerMap = forwardRef(
드래그
+
); diff --git a/front/src/components/Map/ServerMap/MainMap.ts b/front/src/components/Map/ServerMap/MainMap.ts index 2e9e4e2e..eabfcf6e 100644 --- a/front/src/components/Map/ServerMap/MainMap.ts +++ b/front/src/components/Map/ServerMap/MainMap.ts @@ -10,6 +10,7 @@ export class MainMap extends Scene { private isDragging: boolean = false; private dragStartX: number = 0; private dragStartY: number = 0; + private speechBalloon!: Phaser.GameObjects.Text; constructor() { super("MainMap"); @@ -49,6 +50,16 @@ export class MainMap extends Scene { } else { console.log("레이어가 생성되지 않았습니다."); } + // 말풍선 + this.speechBalloon = this.add + .text(this.character.x, this.character.y - 20, "", { + fontSize: 10, + color: "#000", + backgroundColor: "#fff", + padding: { x: 10, y: 10 }, + resolution: 2, + }) + .setOrigin(0.5); } catch (error) { console.error(error); } @@ -87,5 +98,11 @@ export class MainMap extends Scene { } else { this.character.anims.stop(); } + this.speechBalloon.setPosition(this.character.x, this.character.y - 50); + } + setBalloonText(text: string) { + if (this.speechBalloon) { + this.speechBalloon.setText(text); + } } } From 7f9711c278f1b00c7c70e6e3cfac75e271900c48 Mon Sep 17 00:00:00 2001 From: minahYu Date: Sat, 8 Jun 2024 17:40:44 +0900 Subject: [PATCH 060/178] =?UTF-8?q?refac:=20=EB=AF=B8=EB=94=94=EC=96=B4?= =?UTF-8?q?=ED=83=80=EC=9E=85=20enum=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/user/global/SupportedMediaType.kt | 6 ++++++ .../kpring/user/service/UploadProfileImageService.kt | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 user/src/main/kotlin/kpring/user/global/SupportedMediaType.kt diff --git a/user/src/main/kotlin/kpring/user/global/SupportedMediaType.kt b/user/src/main/kotlin/kpring/user/global/SupportedMediaType.kt new file mode 100644 index 00000000..4bdf154a --- /dev/null +++ b/user/src/main/kotlin/kpring/user/global/SupportedMediaType.kt @@ -0,0 +1,6 @@ +package kpring.user.global + +enum class SupportedMediaType(val mediaType: String) { + IMAGE_PNG("image/png"), + IMAGE_JPEG("image/jpeg"), +} diff --git a/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt b/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt index afc45a29..bdf44a5c 100644 --- a/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt +++ b/user/src/main/kotlin/kpring/user/service/UploadProfileImageService.kt @@ -2,6 +2,7 @@ package kpring.user.service import kpring.core.global.exception.ServiceException import kpring.user.exception.UserErrorCode +import kpring.user.global.SupportedMediaType import org.springframework.stereotype.Service import org.springframework.web.multipart.MultipartFile import java.nio.file.Files @@ -14,23 +15,23 @@ class UploadProfileImageService { public fun saveUploadedFile( multipartFile: MultipartFile, userId: Long, - dirPath: Path, + savedPath: Path, ): String { - if (Files.notExists(dirPath)) { - Files.createDirectories(dirPath) + if (Files.notExists(savedPath)) { + Files.createDirectories(savedPath) } val extension = multipartFile.originalFilename!!.substringAfterLast('.') isFileExtensionSupported(multipartFile) val uniqueFileName = generateUniqueFileName(userId, extension) - val filePath = dirPath.resolve(uniqueFileName) + val filePath = savedPath.resolve(uniqueFileName) multipartFile.transferTo(filePath.toFile()) return uniqueFileName } private fun isFileExtensionSupported(multipartFile: MultipartFile) { - val supportedExtensions = listOf("image/png", "image/jpeg") + val supportedExtensions = SupportedMediaType.entries.map { it.mediaType } if (multipartFile.contentType !in supportedExtensions) { throw ServiceException(UserErrorCode.EXTENSION_NOT_SUPPORTED) } From 127fb899eed0ee794ca46f7d3799eb727fa3f9dd Mon Sep 17 00:00:00 2001 From: minahYu Date: Sat, 8 Jun 2024 17:41:13 +0900 Subject: [PATCH 061/178] =?UTF-8?q?remove:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/user/controller/UserControllerTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt index 21553d07..41398594 100644 --- a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt @@ -38,7 +38,6 @@ import org.springframework.restdocs.request.RequestDocumentation.* import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext -import org.springframework.web.multipart.MultipartFile import org.springframework.web.reactive.function.BodyInserters @WebMvcTest(controllers = [UserController::class]) @@ -48,7 +47,6 @@ class UserControllerTest( webContext: WebApplicationContext, @MockkBean val authClient: AuthClient, @MockkBean val userService: UserService, - @MockkBean val multipartFile: MultipartFile, ) : DescribeSpec( { From 1012b0836355650ab7cd83192aa1c3b0838834c3 Mon Sep 17 00:00:00 2001 From: minahYu Date: Sat, 8 Jun 2024 17:42:03 +0900 Subject: [PATCH 062/178] =?UTF-8?q?chore:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/src/main/resources/application.yaml b/user/src/main/resources/application.yaml index c635f869..6b03bfcd 100644 --- a/user/src/main/resources/application.yaml +++ b/user/src/main/resources/application.yaml @@ -17,4 +17,4 @@ auth: file: path: - profile-dir: user/src/main/resources/static/images/profileImg \ No newline at end of file + profile-dir: / \ No newline at end of file From 79a07428edf443e5b347652576030e8fae36fcaf Mon Sep 17 00:00:00 2001 From: minahYu Date: Sat, 8 Jun 2024 17:42:41 +0900 Subject: [PATCH 063/178] =?UTF-8?q?refac:=20=EC=97=90=EB=9F=AC=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=ED=95=9C=EA=B5=AD=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/user/exception/UserErrorCode.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt index dc5dbeec..505752a5 100644 --- a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt +++ b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt @@ -8,14 +8,14 @@ enum class UserErrorCode( val id: String, val message: String, ) : ErrorCode { - BAD_REQUEST(HttpStatus.BAD_REQUEST, "4000", "Invalid email"), - ALREADY_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "4001", "Email already exists"), - NOT_ALLOWED(HttpStatus.FORBIDDEN, "4003", "Not allowed"), // 권한이 없는 경우 + BAD_REQUEST(HttpStatus.BAD_REQUEST, "4000", "이메일 형식이 올바르지 않습니다."), + ALREADY_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "4001", "이미 존재하는 이메일입니다."), + NOT_ALLOWED(HttpStatus.FORBIDDEN, "4003", "권한이 없는 사용자입니다."), // 권한이 없는 경우 - INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "4010", "Incorrect password"), - USER_NOT_FOUND(HttpStatus.NOT_FOUND, "4011", "User not found"), + INCORRECT_PASSWORD(HttpStatus.UNAUTHORIZED, "4010", "비밀번호가 올바르지 않습니다."), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "4011", "사용자를 찾을 수 없습니다."), - EXTENSION_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "4020", "Extension is not supported"), + EXTENSION_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "4020", "지원되지 않는 미디어 유형입니다."), ; override fun message(): String = this.message From 672833e9003da277a08f6b137edd48ae67faee3e Mon Sep 17 00:00:00 2001 From: minahYu Date: Sat, 8 Jun 2024 17:43:02 +0900 Subject: [PATCH 064/178] =?UTF-8?q?refac:=20System.getProperty=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index 7e4d0fa9..ee5e8011 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -40,7 +40,8 @@ class UserServiceImpl( ): UpdateUserProfileResponse { var newPassword: String? = null var uniqueFileName: String? = null - val profileImgDir = Paths.get(System.getProperty("user.dir")).resolve(profileImgDirPath) + val dir = System.getProperty("user.dir") + val profileImgDir = Paths.get(dir) val user = getUser(userId) request.email?.let { handleDuplicateEmail(it) } From f806f10c4b08cddd47c0a8dc66ed786ff75e7a74 Mon Sep 17 00:00:00 2001 From: minahYu Date: Sun, 9 Jun 2024 16:46:28 +0900 Subject: [PATCH 065/178] =?UTF-8?q?fix:=20UserServiceTest=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EC=97=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt index 0aa7ddd4..35ecc9ee 100644 --- a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt @@ -72,7 +72,7 @@ class UserServiceImplTest : FunSpec({ shouldThrow { userService.handleDuplicateEmail(createUserRequest.email) } - exception.errorCode.message() shouldBe "Email already exists" + exception.errorCode.message() shouldBe "이미 존재하는 이메일입니다." verify { userRepository.save(any()) wasNot Called } } From dde725d23468eca0e101cf5f45c3a8b50cf56e60 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 12:41:47 +0900 Subject: [PATCH 066/178] =?UTF-8?q?refac:=20getOrCreateInvitation=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20getChatRoomInvitation=EC=9C=BC=EB=A1=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/service/ChatRoomService.kt | 5 ++++- .../chat/chatroom/service/InvitationService.kt | 15 ++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index b4f37186..b528e3b5 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -37,7 +37,10 @@ class ChatRoomService( userId: String, ): InvitationResponse { verifyChatRoomAccess(chatRoomId, userId) - val code = invitationService.getOrCreateInvitation(userId, chatRoomId) + var code = invitationService.getInvitation(userId, chatRoomId) + if (code == null) { + code = invitationService.setInvitation(userId, chatRoomId) + } val encodedCode = invitationService.generateKeyAndCode(userId, chatRoomId, code) return InvitationResponse(encodedCode) } diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt index 6f7ae4d7..1748b1ca 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt @@ -12,18 +12,7 @@ class InvitationService( private val invitationRepository: InvitationRepository, private val chatRoomProperty: ChatRoomProperty, ) { - fun getOrCreateInvitation( - userId: String, - chatRoomId: String, - ): String { - var code = getInvitation(userId, chatRoomId) - if (code == null) { - code = setInvitation(userId, chatRoomId) - } - return code - } - - private fun getInvitation( + fun getInvitation( userId: String, chatRoomId: String, ): String? { @@ -31,7 +20,7 @@ class InvitationService( return invitationRepository.getValue(key) } - private fun setInvitation( + fun setInvitation( userId: String, chatRoomId: String, ): String { From 15fa38f9253fc1e23f17406f1d8464b56e317dc3 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:15:03 +0900 Subject: [PATCH 067/178] =?UTF-8?q?feat:=20Chat=20model=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20ServerChat=EA=B3=BC=20RoomChat=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/model/Chat.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt new file mode 100644 index 00000000..dd11d349 --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt @@ -0,0 +1,26 @@ +package kpring.chat.chat.model + +import kpring.chat.NoArg +import kpring.chat.global.model.BaseTime +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Document + +@NoArg +@Document(collection = "chats") +class Chat( + val userId: String, + // roomId or serverId + val contextId: String, + var content: String, +) : BaseTime() { + @Id + var id: String? = null + + fun isEdited(): Boolean { + return !createdAt.equals(updatedAt) + } + + fun updateContent(content: String) { + this.content = content + } +} From 24f64a3e06e5dd488d6904f62f6f7b0c6db424d6 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:16:07 +0900 Subject: [PATCH 068/178] =?UTF-8?q?refac:=20Chat=20model=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20ServerChat=EA=B3=BC=20RoomChat=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/service/ChatService.kt | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index 81412cae..b2b19418 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -1,7 +1,6 @@ package kpring.chat.chat.service -import kpring.chat.chat.model.RoomChat -import kpring.chat.chat.model.ServerChat +import kpring.chat.chat.model.Chat import kpring.chat.chat.repository.RoomChatRepository import kpring.chat.chat.repository.ServerChatRepository import kpring.chat.chatroom.repository.ChatRoomRepository @@ -28,9 +27,9 @@ class ChatService( ): Boolean { val chat = roomChatRepository.save( - RoomChat( + Chat( userId = userId, - roomId = request.id, + contextId = request.id, content = request.content, ), ) @@ -43,9 +42,9 @@ class ChatService( ): Boolean { val chat = serverChatRepository.save( - ServerChat( + Chat( userId = userId, - serverId = request.id, + contextId = request.id, content = request.content, ), ) @@ -60,9 +59,9 @@ class ChatService( verifyChatRoomAccess(chatRoomId, userId) val pageable: Pageable = PageRequest.of(page, pageSize) - val roomChats: List = roomChatRepository.findAllByRoomId(chatRoomId, pageable) + val roomChats: List = roomChatRepository.findAllByContextId(chatRoomId, pageable) - return convertRoomChatsToResponses(roomChats) + return convertChatsToResponses(roomChats) } fun getServerChats( @@ -74,9 +73,9 @@ class ChatService( verifyServerAccess(servers, serverId) val pageable: Pageable = PageRequest.of(page, pageSize) - val chats: List = serverChatRepository.findAllByServerId(serverId, pageable) + val chats: List = serverChatRepository.findAllByContextId(serverId, pageable) - return convertServerChatsToResponses(chats) + return convertChatsToResponses(chats) } private fun verifyServerAccess( @@ -100,15 +99,7 @@ class ChatService( } } - private fun convertRoomChatsToResponses(roomChats: List): List { - val chatResponse = - roomChats.map { chat -> - ChatResponse(chat.id!!, chat.isEdited(), chat.createdAt.toString(), chat.content) - } - return chatResponse - } - - private fun convertServerChatsToResponses(chats: List): List { + private fun convertChatsToResponses(chats: List): List { val chatResponse = chats.map { chat -> ChatResponse(chat.id!!, chat.isEdited(), chat.createdAt.toString(), chat.content) From 868210fe0f039d5ce5b9f1373287ef35cf19c510 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:16:14 +0900 Subject: [PATCH 069/178] =?UTF-8?q?refac:=20Chat=20model=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20ServerChat=EA=B3=BC=20RoomChat=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index 4ae96ba3..eca8b15a 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -6,7 +6,7 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify -import kpring.chat.chat.model.RoomChat +import kpring.chat.chat.model.Chat import kpring.chat.chat.repository.RoomChatRepository import kpring.chat.chat.repository.ServerChatRepository import kpring.chat.chat.service.ChatService @@ -33,7 +33,7 @@ class ChatServiceTest( // Given val request = CreateChatRequest(id = ChatRoomTest.TEST_ROOM_ID, content = ChatTest.CONTENT, type = ChatType.Room) val userId = CommonTest.TEST_USER_ID - val roomChat = RoomChat(userId, request.id, request.content) + val roomChat = Chat(userId, request.id, request.content) every { roomChatRepository.save(any()) } returns roomChat // When From 3485157a81ed855ae03286af1dfd432f1528c7e2 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:16:59 +0900 Subject: [PATCH 070/178] =?UTF-8?q?refac:=20RoomChatRepository=EA=B0=80=20?= =?UTF-8?q?RoomChat=EB=8C=80=EC=8B=A0=20Chat=EC=9D=84=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/repository/RoomChatRepository.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/repository/RoomChatRepository.kt b/chat/src/main/kotlin/kpring/chat/chat/repository/RoomChatRepository.kt index f8506d1e..53dc1d2f 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/repository/RoomChatRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/repository/RoomChatRepository.kt @@ -1,15 +1,15 @@ package kpring.chat.chat.repository -import kpring.chat.chat.model.RoomChat +import kpring.chat.chat.model.Chat import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.data.querydsl.QuerydslPredicateExecutor import org.springframework.stereotype.Repository @Repository -interface RoomChatRepository : MongoRepository, QuerydslPredicateExecutor { - fun findAllByRoomId( - roomId: String, +interface RoomChatRepository : MongoRepository, QuerydslPredicateExecutor { + fun findAllByContextId( + contextId: String, pageable: Pageable, - ): List + ): List } From 44957fa6fcef970140ca24e77a83e6ea7c7a658f Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:17:13 +0900 Subject: [PATCH 071/178] =?UTF-8?q?refac:=20ServerChatRepository=EA=B0=80?= =?UTF-8?q?=20ServerChat=EB=8C=80=EC=8B=A0=20Chat=EC=9D=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/repository/ServerChatRepository.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/repository/ServerChatRepository.kt b/chat/src/main/kotlin/kpring/chat/chat/repository/ServerChatRepository.kt index 7bc1ad42..212d8c32 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/repository/ServerChatRepository.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/repository/ServerChatRepository.kt @@ -1,15 +1,15 @@ package kpring.chat.chat.repository -import kpring.chat.chat.model.ServerChat +import kpring.chat.chat.model.Chat import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.MongoRepository import org.springframework.data.querydsl.QuerydslPredicateExecutor import org.springframework.stereotype.Repository @Repository -interface ServerChatRepository : MongoRepository, QuerydslPredicateExecutor { - fun findAllByServerId( +interface ServerChatRepository : MongoRepository, QuerydslPredicateExecutor { + fun findAllByContextId( serverId: String, pageable: Pageable, - ): List + ): List } From 9b2090a2971d58332a8d29a3a1d06020b6a00bb1 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:17:37 +0900 Subject: [PATCH 072/178] =?UTF-8?q?remove:=20RoomChat=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/model/RoomChat.kt | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 chat/src/main/kotlin/kpring/chat/chat/model/RoomChat.kt diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/RoomChat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/RoomChat.kt deleted file mode 100644 index c11c2290..00000000 --- a/chat/src/main/kotlin/kpring/chat/chat/model/RoomChat.kt +++ /dev/null @@ -1,21 +0,0 @@ -package kpring.chat.chat.model - -import kpring.chat.NoArg -import kpring.chat.global.model.BaseTime -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document - -@NoArg -@Document(collection = "chats") -class RoomChat( - val userId: String, - val roomId: String, - val content: String, -) : BaseTime() { - @Id - var id: String? = null - - fun isEdited(): Boolean { - return !createdAt.equals(updatedAt) - } -} From e974fa2d9d4f50b12e9f06f8bc040c3833acbe70 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:17:49 +0900 Subject: [PATCH 073/178] =?UTF-8?q?remove:=20ServerChat=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/model/ServerChat.kt | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 chat/src/main/kotlin/kpring/chat/chat/model/ServerChat.kt diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/ServerChat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/ServerChat.kt deleted file mode 100644 index 5eef055b..00000000 --- a/chat/src/main/kotlin/kpring/chat/chat/model/ServerChat.kt +++ /dev/null @@ -1,21 +0,0 @@ -package kpring.chat.chat.model - -import kpring.chat.NoArg -import kpring.chat.global.model.BaseTime -import org.springframework.data.annotation.Id -import org.springframework.data.mongodb.core.mapping.Document - -@NoArg -@Document(collection = "server_chats") -class ServerChat( - val userId: String, - val serverId: String, - val content: String, -) : BaseTime() { - @Id - var id: String? = null - - fun isEdited(): Boolean { - return !createdAt.equals(updatedAt) - } -} From 1a0e342d540553de6464546f7344d59bf1b6ef58 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 14:18:14 +0900 Subject: [PATCH 074/178] =?UTF-8?q?refac:=20RoomChat=EC=9D=84=20Chat?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/chat/example/SampleTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt b/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt index 2fa7d1f7..9cf375e3 100644 --- a/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt +++ b/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt @@ -3,8 +3,8 @@ package kpring.chat.example import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import kpring.chat.chat.model.QRoomChat -import kpring.chat.chat.model.RoomChat +import kpring.chat.chat.model.Chat +import kpring.chat.chat.model.QChat import kpring.chat.chat.repository.RoomChatRepository import kpring.test.testcontainer.SpringTestContext import org.springframework.boot.test.context.SpringBootTest @@ -25,10 +25,10 @@ class SampleTest( it("query dsl 적용 테스트") { // given - val chat = QRoomChat.roomChat + val chat = QChat.chat repeat(5) { idx -> roomChatRepository.save( - RoomChat("testUserId", "testRoomId", "testContent$idx"), + Chat("testUserId", "testRoomId", "testContent$idx"), ) } @@ -49,11 +49,11 @@ class SampleTest( it("query dsl 적용 테스트 : 다중 조건") { // given - val chat = QRoomChat.roomChat + val chat = QChat.chat roomChatRepository.deleteAll() repeat(5) { idx -> roomChatRepository.save( - RoomChat("testUserId", "testRoomId", "testContent$idx"), + Chat("testUserId", "testRoomId", "testContent$idx"), ) } From 1a8ef8d90be1e8833e66ccf7dbc490705834c3f1 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 16:37:52 +0900 Subject: [PATCH 075/178] =?UTF-8?q?feat:=20Chat=EC=97=90=20verifyAccess=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt index dd11d349..ffd0d432 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt @@ -1,6 +1,8 @@ package kpring.chat.chat.model import kpring.chat.NoArg +import kpring.chat.global.exception.ErrorCode +import kpring.chat.global.exception.GlobalException import kpring.chat.global.model.BaseTime import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @@ -23,4 +25,10 @@ class Chat( fun updateContent(content: String) { this.content = content } + + fun verifyAccess(userId: String) { + if (userId != this.userId) { + throw GlobalException(ErrorCode.FORBIDDEN_CHAT) + } + } } From 9a0f8617e48bdda91c4d517d1f9a7b3c4da9dca5 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:01:49 +0900 Subject: [PATCH 076/178] =?UTF-8?q?feat:=20updateChat=20Controller=20Layer?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/api/v1/ChatController.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt index bb2caffc..27361f4d 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt @@ -6,6 +6,7 @@ import kpring.chat.global.exception.GlobalException import kpring.core.auth.client.AuthClient import kpring.core.chat.chat.dto.request.ChatType import kpring.core.chat.chat.dto.request.CreateChatRequest +import kpring.core.chat.chat.dto.request.UpdateChatRequest import kpring.core.global.dto.response.ApiResponse import kpring.core.server.client.ServerClient import kpring.core.server.dto.request.GetServerCondition @@ -59,4 +60,19 @@ class ChatController( } return ResponseEntity.ok().body(ApiResponse(data = result, status = 200)) } + + @PatchMapping("/chat") + fun updateChat( + @Validated @RequestBody request: UpdateChatRequest, + @RequestHeader("Authorization") token: String, + ): ResponseEntity<*> { + val userId = authClient.getTokenInfo(token).data!!.userId + val result = + when (request.type) { + ChatType.Room -> chatService.updateRoomChat(request, userId) + ChatType.Server -> chatService.updateServerChat(request, userId) + else -> throw GlobalException(ErrorCode.INVALID_CHAT_TYPE) + } + return ResponseEntity.ok().body(ApiResponse(status = 200)) + } } From c638d6b3cee9bca9bfed25fe85ae967b7f35f49c Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:02:33 +0900 Subject: [PATCH 077/178] =?UTF-8?q?feat:=20updateRoomChat,updateServerChat?= =?UTF-8?q?=20ChatService=EC=97=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/service/ChatService.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index b2b19418..4a1c6402 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -7,6 +7,7 @@ import kpring.chat.chatroom.repository.ChatRoomRepository import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.request.CreateChatRequest +import kpring.core.chat.chat.dto.request.UpdateChatRequest import kpring.core.chat.chat.dto.response.ChatResponse import kpring.core.server.dto.ServerSimpleInfo import org.springframework.beans.factory.annotation.Value @@ -78,6 +79,28 @@ class ChatService( return convertChatsToResponses(chats) } + fun updateRoomChat( + request: UpdateChatRequest, + userId: String, + ): Boolean { + val chat = roomChatRepository.findById(request.id).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } + chat.verifyAccess(userId) + chat.updateContent(request.content) + roomChatRepository.save(chat) + return true + } + + fun updateServerChat( + request: UpdateChatRequest, + userId: String, + ): Boolean { + val chat = serverChatRepository.findById(request.id).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } + chat.verifyAccess(userId) + chat.updateContent(request.content) + serverChatRepository.save(chat) + return true + } + private fun verifyServerAccess( servers: List, serverId: String, From fcafda795acb2c2400cdba89692c07eb1f17b259 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:02:51 +0900 Subject: [PATCH 078/178] =?UTF-8?q?feat:=20CHAT=5FNOT=5FFOUND=20ErrorCode?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt index 09798d7a..245614e8 100644 --- a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt +++ b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt @@ -9,7 +9,9 @@ enum class ErrorCode(val httpStatus: Int, val message: String) { // 403 FORBIDDEN_CHATROOM(HttpStatus.FORBIDDEN.value(), "접근이 제한된 채팅방 입니다"), FORBIDDEN_SERVER(HttpStatus.FORBIDDEN.value(), "접근이 제한된 서버 입니다"), + FORBIDDEN_CHAT(HttpStatus.FORBIDDEN.value(), "접근이 제한된 채팅 입니다"), // 404 CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"), + CHAT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 채팅을 찾을 수 없습니다"), } From 1865a58e9cc5855ac12431f7d675f394fd8990b9 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:03:03 +0900 Subject: [PATCH 079/178] =?UTF-8?q?feat:=20UpdateChatRequest=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/chat/chat/dto/request/UpdateChatRequest.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 core/src/main/kotlin/kpring/core/chat/chat/dto/request/UpdateChatRequest.kt diff --git a/core/src/main/kotlin/kpring/core/chat/chat/dto/request/UpdateChatRequest.kt b/core/src/main/kotlin/kpring/core/chat/chat/dto/request/UpdateChatRequest.kt new file mode 100644 index 00000000..65ded789 --- /dev/null +++ b/core/src/main/kotlin/kpring/core/chat/chat/dto/request/UpdateChatRequest.kt @@ -0,0 +1,11 @@ +package kpring.core.chat.chat.dto.request + +import jakarta.validation.constraints.NotNull + +data class UpdateChatRequest( + @field:NotNull + val id: String, + @field:NotNull + val type: ChatType, + val content: String, +) From 5e609fe22e692bd12c3232f46a043079f7e941f2 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:53:10 +0900 Subject: [PATCH 080/178] =?UTF-8?q?feat:=20MongoAuditing=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/ChatApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/ChatApplication.kt b/chat/src/main/kotlin/kpring/chat/ChatApplication.kt index 18906f4a..7e6d9407 100644 --- a/chat/src/main/kotlin/kpring/chat/ChatApplication.kt +++ b/chat/src/main/kotlin/kpring/chat/ChatApplication.kt @@ -2,7 +2,9 @@ package kpring.chat import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.data.mongodb.config.EnableMongoAuditing +@EnableMongoAuditing @SpringBootApplication class ChatApplication From a9a675603f764e5f8b083b2079ad578d513da76b Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:53:45 +0900 Subject: [PATCH 081/178] =?UTF-8?q?chore:=20MongoAuditing=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/resources/application.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/chat/src/main/resources/application.yml b/chat/src/main/resources/application.yml index 0590f9e8..23bfa40b 100644 --- a/chat/src/main/resources/application.yml +++ b/chat/src/main/resources/application.yml @@ -10,6 +10,13 @@ spring: database: mongodb authentication-database: admin authSource: admin + repositories: + enabled: true + + redis: + host: localhost + port: 6379 + password: testpassword1234 server: port: 8081 @@ -17,6 +24,10 @@ server: auth: url: "http://localhost:30000/" url: - server: "http://localhost:8080/" + server: "http://localhost:8080/" + page: size: 100 +chatroom: + expiration: 1d + baseurl: "http://localhost:8081/" From 8c823ba027a3703f59f5dd4c8db375ccfbbf5c12 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 17:53:52 +0900 Subject: [PATCH 082/178] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/controller/UserController.kt | 35 ++++--------------- .../kpring/user/global/AuthValidator.kt | 30 ++++++++++++++++ 2 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 user/src/main/kotlin/kpring/user/global/AuthValidator.kt diff --git a/user/src/main/kotlin/kpring/user/controller/UserController.kt b/user/src/main/kotlin/kpring/user/controller/UserController.kt index 8c4c78a2..a13a4aa2 100644 --- a/user/src/main/kotlin/kpring/user/controller/UserController.kt +++ b/user/src/main/kotlin/kpring/user/controller/UserController.kt @@ -1,15 +1,12 @@ package kpring.user.controller -import kpring.core.auth.client.AuthClient -import kpring.core.auth.enums.TokenType import kpring.core.global.dto.response.ApiResponse -import kpring.core.global.exception.ServiceException import kpring.user.dto.request.CreateUserRequest import kpring.user.dto.request.UpdateUserProfileRequest import kpring.user.dto.response.CreateUserResponse import kpring.user.dto.response.GetUserProfileResponse import kpring.user.dto.response.UpdateUserProfileResponse -import kpring.user.exception.UserErrorCode +import kpring.user.global.AuthValidator import kpring.user.service.UserService import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated @@ -20,14 +17,14 @@ import org.springframework.web.multipart.MultipartFile @RequestMapping("/api/v1") class UserController( val userService: UserService, - val authClient: AuthClient, + val authValidator: AuthValidator, ) { @GetMapping("/user/{userId}") fun getUserProfile( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { - checkIfAccessTokenAndGetUserId(token) + authValidator.checkIfAccessTokenAndGetUserId(token) val response = userService.getProfile(userId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -47,8 +44,8 @@ class UserController( @Validated @RequestPart(value = "json") request: UpdateUserProfileRequest, @RequestPart(value = "file") multipartFile: MultipartFile, ): ResponseEntity> { - val validatedUserId = checkIfAccessTokenAndGetUserId(token) - checkIfUserIsSelf(userId.toString(), validatedUserId) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val response = userService.updateProfile(userId, request, multipartFile) return ResponseEntity.ok(ApiResponse(data = response)) @@ -59,8 +56,8 @@ class UserController( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { - val validatedUserId = checkIfAccessTokenAndGetUserId(token) - checkIfUserIsSelf(userId.toString(), validatedUserId) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val isExit = userService.exitUser(userId) @@ -70,22 +67,4 @@ class UserController( ResponseEntity.badRequest().build() } } - - private fun checkIfAccessTokenAndGetUserId(token: String): String { - val validationResult = authClient.getTokenInfo(token) - if (validationResult.data!!.type != TokenType.ACCESS) { - throw ServiceException(UserErrorCode.BAD_REQUEST) - } - - return validationResult.data!!.userId - } - - private fun checkIfUserIsSelf( - userId: String, - validatedUserId: String, - ) { - if (userId != validatedUserId) { - throw ServiceException(UserErrorCode.NOT_ALLOWED) - } - } } diff --git a/user/src/main/kotlin/kpring/user/global/AuthValidator.kt b/user/src/main/kotlin/kpring/user/global/AuthValidator.kt new file mode 100644 index 00000000..33f652e3 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/global/AuthValidator.kt @@ -0,0 +1,30 @@ +package kpring.user.global + +import kpring.core.auth.client.AuthClient +import kpring.core.auth.enums.TokenType +import kpring.core.global.exception.ServiceException +import kpring.user.exception.UserErrorCode +import org.springframework.stereotype.Component + +@Component +class AuthValidator( + private val authClient: AuthClient, +) { + fun checkIfAccessTokenAndGetUserId(token: String): String { + val validationResult = authClient.getTokenInfo(token) + if (validationResult.data!!.type != TokenType.ACCESS) { + throw ServiceException(UserErrorCode.BAD_REQUEST) + } + + return validationResult.data!!.userId + } + + fun checkIfUserIsSelf( + userId: String, + validatedUserId: String, + ) { + if (userId != validatedUserId) { + throw ServiceException(UserErrorCode.NOT_ALLOWED) + } + } +} From a5c191c9d71e6b9966c8b2927f841009ceb794d5 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 17:55:13 +0900 Subject: [PATCH 083/178] =?UTF-8?q?chore:=20FriendService=20=EC=9E=90?= =?UTF-8?q?=EB=B0=94=EC=97=90=EC=84=9C=20=EC=BD=94=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/service/FriendService.java | 40 ------------------- .../kpring/user/service/FriendService.kt | 19 +++++++++ .../kpring/user/service/FriendServiceImpl.kt | 29 ++++++++++++++ 3 files changed, 48 insertions(+), 40 deletions(-) delete mode 100644 user/src/main/java/kpring/user/service/FriendService.java create mode 100644 user/src/main/kotlin/kpring/user/service/FriendService.kt create mode 100644 user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt diff --git a/user/src/main/java/kpring/user/service/FriendService.java b/user/src/main/java/kpring/user/service/FriendService.java deleted file mode 100644 index 2edc4128..00000000 --- a/user/src/main/java/kpring/user/service/FriendService.java +++ /dev/null @@ -1,40 +0,0 @@ -package kpring.user.service; - -import kpring.user.dto.request.AddFriendRequest; -import kpring.user.dto.result.AddFriendResponse; -import kpring.user.dto.response.DeleteFriendResponse; -import kpring.user.dto.response.GetFriendsResponse; -import kpring.user.repository.UserRepository; -import org.springframework.stereotype.Service; - -@Service -public class FriendService { - - private final UserRepository userRepository; - - public FriendService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public GetFriendsResponse getFriends(Long userId) { - return null; - } - - public AddFriendResponse addFriend(AddFriendRequest friendsRequestDto, Long userId) { - var user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("User not found")); - - user.getFollowers().forEach(follower -> { - follower.getFollowees().add(user); - }); - - user.getFollowees().forEach(follower -> { - follower.getFollowers().add(user); - }); - return new AddFriendResponse(friendsRequestDto.getFriendId()); - } - - public DeleteFriendResponse deleteFriend(Long userId, Long friendId) { - return null; - } -} diff --git a/user/src/main/kotlin/kpring/user/service/FriendService.kt b/user/src/main/kotlin/kpring/user/service/FriendService.kt new file mode 100644 index 00000000..8dee2657 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/service/FriendService.kt @@ -0,0 +1,19 @@ +package kpring.user.service + +import kpring.user.dto.response.DeleteFriendResponse +import kpring.user.dto.response.GetFriendsResponse +import kpring.user.dto.result.AddFriendResponse + +interface FriendService { + fun getFriends(userId: Long): GetFriendsResponse + + fun addFriend( + userId: Long, + friendId: Long, + ): AddFriendResponse + + fun deleteFriend( + userId: Long, + friendId: Long, + ): DeleteFriendResponse +} diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt new file mode 100644 index 00000000..67a8e412 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -0,0 +1,29 @@ +package kpring.user.service + +import kpring.user.dto.response.DeleteFriendResponse +import kpring.user.dto.response.GetFriendsResponse +import kpring.user.dto.result.AddFriendResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class FriendServiceImpl() : FriendService { + override fun getFriends(userId: Long): GetFriendsResponse { + TODO("Not yet implemented") + } + + override fun addFriend( + userId: Long, + friendId: Long, + ): AddFriendResponse { + TODO("Not yet implemented") + } + + override fun deleteFriend( + userId: Long, + friendId: Long, + ): DeleteFriendResponse { + TODO("Not yet implemented") + } +} From 9730302e0bf8ab879a18cc3e5ca6201bb4625056 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:55:36 +0900 Subject: [PATCH 084/178] =?UTF-8?q?feat:=20updateChat=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chat/api/v1/ChatControllerTest.kt | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt index 77a71e77..7d36fa65 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt @@ -13,6 +13,7 @@ import kpring.core.auth.dto.response.TokenInfo import kpring.core.auth.enums.TokenType import kpring.core.chat.chat.dto.request.ChatType import kpring.core.chat.chat.dto.request.CreateChatRequest +import kpring.core.chat.chat.dto.request.UpdateChatRequest import kpring.core.chat.chat.dto.response.ChatResponse import kpring.core.global.dto.response.ApiResponse import kpring.core.server.client.ServerClient @@ -280,4 +281,120 @@ class ChatControllerTest( } } } + + describe("PATCH /api/v1/chat : updateChat api test") { + + val url = "/api/v1/chat" + it("updateRoomChat api test") { + + // Given + val roomId = "test_room_id" + val content = "edit test" + val request = UpdateChatRequest(id = roomId, type = ChatType.Room, content = content) + val userId = CommonTest.TEST_USER_ID + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = userId, + ), + ) + + every { + chatService.updateRoomChat( + request, + CommonTest.TEST_USER_ID, + ) + } returns true + + // When + val result = + webTestClient.patch().uri(url) + .bodyValue(request) + .header("Authorization", "Bearer mock_token") + .exchange() + + val docs = + result + .expectStatus() + .isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(status = 200))) + + // Then + docs.restDoc( + identifier = "update_chats_200", + description = "채팅방 채팅 업데이트 api", + ) { + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + it("updateServerChat api test") { + + // Given + val serverId = "test_server_id" + val content = "edit test" + val request = UpdateChatRequest(id = serverId, type = ChatType.Server, content = content) + val userId = CommonTest.TEST_USER_ID + + val serverList = + listOf( + ServerSimpleInfo( + id = serverId, + name = "test_server_name", + bookmarked = true, + ), + ) + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = userId, + ), + ) + + every { serverClient.getServerList(any(), any()) } returns + ResponseEntity.ok().body(ApiResponse(data = serverList)) + + every { + chatService.updateServerChat( + request, + userId, + ) + } returns + true + + // When + val result = + webTestClient.patch().uri(url) + .bodyValue(request) + .header("Authorization", "Bearer mock_token") + .exchange() + + val docs = + result + .expectStatus() + .isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(status = 200))) + + // Then + docs.restDoc( + identifier = "update_chats_200", + description = "서버 채팅 업데이트 api", + ) { + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + } }) From 3aa030663d4af219e49bf6692af545312c108354 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 17:55:42 +0900 Subject: [PATCH 085/178] =?UTF-8?q?feat:=20updateChat=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/ChatServiceTest.kt | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index eca8b15a..c75112c2 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -18,8 +18,10 @@ import kpring.chat.global.exception.ErrorCode import kpring.chat.global.exception.GlobalException import kpring.core.chat.chat.dto.request.ChatType import kpring.core.chat.chat.dto.request.CreateChatRequest +import kpring.core.chat.chat.dto.request.UpdateChatRequest import kpring.core.server.dto.ServerSimpleInfo import org.springframework.beans.factory.annotation.Value +import java.util.* class ChatServiceTest( @Value("\${page.size}") val pageSize: Int = 100, @@ -85,4 +87,109 @@ class ChatServiceTest( val errorCode = errorCodeField.get(exception) as ErrorCode errorCode shouldBe ErrorCode.FORBIDDEN_SERVER } + + test("updateRoomChat 은 권한이 없는 사용자에게 에러 발생") { + // Given + val roomId = "test_room_id" + val chatId = "test_chat_id" + val contentUpdate = "content update" + val request = UpdateChatRequest(id = chatId, type = ChatType.Room, content = contentUpdate) + val anotherUserId = CommonTest.TEST_ANOTHER_USER_ID + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + roomId, + "content", + ) + + every { roomChatRepository.findById(request.id) } returns Optional.of(chat) + + // When & Then + val exception = + shouldThrow { + chatService.updateRoomChat(request, anotherUserId) + } + val errorCodeField = GlobalException::class.java.getDeclaredField("errorCode") + errorCodeField.isAccessible = true + val errorCode = errorCodeField.get(exception) as ErrorCode + errorCode shouldBe ErrorCode.FORBIDDEN_CHAT + } + + test("updateServerChat 은 권한이 없는 사용자에게 에러 발생") { + // Given + val serverId = "test_server_id" + val chatId = "test_chat_id" + val contentUpdate = "content update" + val request = UpdateChatRequest(id = chatId, type = ChatType.Server, content = contentUpdate) + val anotherUserId = CommonTest.TEST_ANOTHER_USER_ID + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + serverId, + "content", + ) + + every { serverChatRepository.findById(request.id) } returns Optional.of(chat) + + // When & Then + val exception = + shouldThrow { + chatService.updateServerChat(request, anotherUserId) + } + val errorCodeField = GlobalException::class.java.getDeclaredField("errorCode") + errorCodeField.isAccessible = true + val errorCode = errorCodeField.get(exception) as ErrorCode + errorCode shouldBe ErrorCode.FORBIDDEN_CHAT + } + + test("updateRoomChat 은 권한이 있는 사용자의 요청에 따라 Chat 수정") { + // Given + val roomId = "test_room_id" + val chatId = "test_chat_id" + val contentUpdate = "content update" + val request = UpdateChatRequest(id = chatId, type = ChatType.Room, content = contentUpdate) + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + roomId, + "content", + ) + + every { roomChatRepository.findById(request.id) } returns Optional.of(chat) + + // When + val result = chatService.updateRoomChat(request, userId) + + // Then + result shouldBe true + verify { roomChatRepository.save(any()) } + } + + test("updateServerChat 은 권한이 있는 사용자의 요청에 따라 Chat 수정") { + // Given + val serverId = "test_server_id" + val chatId = "test_chat_id" + val contentUpdate = "content update" + val request = UpdateChatRequest(id = chatId, type = ChatType.Server, content = contentUpdate) + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + serverId, + "content", + ) + val updated = + + every { serverChatRepository.findById(request.id) } returns Optional.of(chat) + + // When + val result = chatService.updateRoomChat(request, userId) + + // Then + result shouldBe true + verify { roomChatRepository.save(any()) } + } }) From 7124224ff8e10c22af71fbdb01dd6e7191e3a060 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 17:59:12 +0900 Subject: [PATCH 086/178] =?UTF-8?q?feat:=20FriendController=20=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/controller/FriendController.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/controller/FriendController.kt b/user/src/main/kotlin/kpring/user/controller/FriendController.kt index ae6ff72d..c41efb36 100644 --- a/user/src/main/kotlin/kpring/user/controller/FriendController.kt +++ b/user/src/main/kotlin/kpring/user/controller/FriendController.kt @@ -1,24 +1,29 @@ package kpring.user.controller import kpring.core.global.dto.response.ApiResponse -import kpring.user.dto.request.AddFriendRequest import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse import kpring.user.dto.result.AddFriendResponse +import kpring.user.global.AuthValidator import kpring.user.service.FriendService import org.springframework.http.ResponseEntity -import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* @RestController @RequestMapping("/api/v1") -class FriendController(val friendService: FriendService) { +class FriendController( + private val friendService: FriendService, + private val authValidator: AuthValidator, +) { @PostMapping("/user/{userId}/friend/{friendId}") fun addFriend( + @RequestHeader("Authorization") token: String, @PathVariable userId: Long, - @Validated @RequestBody request: AddFriendRequest, + @PathVariable friendId: Long, ): ResponseEntity> { - val response = friendService.addFriend(request, userId) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) + val response = friendService.addFriend(userId, friendId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -27,6 +32,7 @@ class FriendController(val friendService: FriendService) { @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { + authValidator.checkIfAccessTokenAndGetUserId(token) val response = friendService.getFriends(userId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -37,6 +43,8 @@ class FriendController(val friendService: FriendService) { @PathVariable userId: Long, @PathVariable friendId: Long, ): ResponseEntity> { + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val response = friendService.deleteFriend(userId, friendId) return ResponseEntity.ok(ApiResponse(data = response)) } From 854b8481ed3fb8ec11be444f1a65fd75eed5c9b7 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:23:11 +0900 Subject: [PATCH 087/178] =?UTF-8?q?feat:=20=EC=B9=9C=EA=B5=AC=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20FriendRequestStatus=20enum=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/FriendRequestStatus.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt diff --git a/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt b/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt new file mode 100644 index 00000000..2c4667ef --- /dev/null +++ b/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt @@ -0,0 +1,7 @@ +package kpring.user.entity + +enum class FriendRequestStatus { + REQUESTED, + RECEIVED, + ACCEPTED, +} From 623896bb668d12a45101a17681df293b0c5dc6af Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:24:24 +0900 Subject: [PATCH 088/178] =?UTF-8?q?feat:=20=EC=B9=9C=EA=B5=AC=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt index 505752a5..e0dea1b5 100644 --- a/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt +++ b/user/src/main/kotlin/kpring/user/exception/UserErrorCode.kt @@ -16,6 +16,9 @@ enum class UserErrorCode( USER_NOT_FOUND(HttpStatus.NOT_FOUND, "4011", "사용자를 찾을 수 없습니다."), EXTENSION_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "4020", "지원되지 않는 미디어 유형입니다."), + + ALREADY_FRIEND(HttpStatus.BAD_REQUEST, "4030", "이미 친구입니다."), + NOT_SELF_FOLLOW(HttpStatus.BAD_REQUEST, "4031", "자기자신에게 친구요청을 보낼 수 없습니다"), ; override fun message(): String = this.message From c9e40d69998832e7a37e9cd015390de408a927a9 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:24:44 +0900 Subject: [PATCH 089/178] =?UTF-8?q?feat:=20Friend=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/Friend.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 user/src/main/kotlin/kpring/user/entity/Friend.kt diff --git a/user/src/main/kotlin/kpring/user/entity/Friend.kt b/user/src/main/kotlin/kpring/user/entity/Friend.kt new file mode 100644 index 00000000..00d2754b --- /dev/null +++ b/user/src/main/kotlin/kpring/user/entity/Friend.kt @@ -0,0 +1,20 @@ +package kpring.user.entity + +import jakarta.persistence.* + +@Entity +@Table(name = "tb_friend") +class Friend( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + var user: User, + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "friend_id") + var friend: User, + @Enumerated(EnumType.STRING) + @Column(nullable = false) + var requestStatus: FriendRequestStatus, +) From 1a3f132c1fec8b82ea9a316765f97e9f02184927 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:25:16 +0900 Subject: [PATCH 090/178] =?UTF-8?q?feat:=20Friend=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/user/repository/FriendRepository.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 user/src/main/kotlin/kpring/user/repository/FriendRepository.kt diff --git a/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt new file mode 100644 index 00000000..ae0173b4 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt @@ -0,0 +1,11 @@ +package kpring.user.repository + +import kpring.user.entity.Friend +import org.springframework.data.jpa.repository.JpaRepository + +interface FriendRepository : JpaRepository { + fun existsByUserIdAndFriendId( + userId: Long, + friendId: Long, + ): Boolean +} From 3171bc7d642d6687cbbdae646a09abddf643b977 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:27:08 +0900 Subject: [PATCH 091/178] =?UTF-8?q?feat:=20User=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EC=B9=9C=EA=B5=AC=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/User.kt | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/User.kt b/user/src/main/kotlin/kpring/user/entity/User.kt index 0854691b..2ed8275e 100644 --- a/user/src/main/kotlin/kpring/user/entity/User.kt +++ b/user/src/main/kotlin/kpring/user/entity/User.kt @@ -16,25 +16,16 @@ class User( @Column(nullable = false) var password: String, var file: String?, - @ManyToMany(fetch = FetchType.LAZY) - @JoinTable( - name = "user_followers", - joinColumns = [JoinColumn(name = "user_id")], - inverseJoinColumns = [JoinColumn(name = "follower_id")], - ) - val followers: MutableSet = mutableSetOf(), - @ManyToMany(mappedBy = "followers", fetch = FetchType.LAZY) - val followees: MutableSet = mutableSetOf(), + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = [CascadeType.ALL]) + val friends: MutableSet = mutableSetOf(), // Other fields and methods... ) { - fun addFollower(follower: User) { - followers.add(follower) - follower.followees.add(this) - } - - fun removeFollower(follower: User) { - followers.remove(follower) - follower.followees.remove(this) + fun addFriendRelation( + friend: User, + requestStatus: FriendRequestStatus, + ) { + val friendRelation = Friend(user = this, friend = friend, requestStatus = requestStatus) + friends.add(friendRelation) } fun updateInfo( From afd73a7e6505ab0ea0b40b66016b379f1a76a69f Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 18:45:17 +0900 Subject: [PATCH 092/178] =?UTF-8?q?feat:=20FriendServiceImpl=EC=97=90=20?= =?UTF-8?q?=EC=B9=9C=EA=B5=AC=EC=8B=A0=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/service/FriendServiceImpl.kt | 38 ++++++++++++++++++- .../kpring/user/service/UserServiceImpl.kt | 2 +- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index 67a8e412..bb79fdbc 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -1,14 +1,22 @@ package kpring.user.service +import kpring.core.global.exception.ServiceException import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse import kpring.user.dto.result.AddFriendResponse +import kpring.user.entity.FriendRequestStatus +import kpring.user.entity.User +import kpring.user.exception.UserErrorCode +import kpring.user.repository.FriendRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service @Transactional -class FriendServiceImpl() : FriendService { +class FriendServiceImpl( + private val userServiceImpl: UserServiceImpl, + private val friendRepository: FriendRepository, +) : FriendService { override fun getFriends(userId: Long): GetFriendsResponse { TODO("Not yet implemented") } @@ -17,7 +25,15 @@ class FriendServiceImpl() : FriendService { userId: Long, friendId: Long, ): AddFriendResponse { - TODO("Not yet implemented") + val user = userServiceImpl.getUser(userId) + val friend = userServiceImpl.getUser(friendId) + + checkSelfFriend(user, friend) + checkFriendRelationExists(userId, friendId) + user.addFriendRelation(friend, FriendRequestStatus.REQUESTED) + friend.addFriendRelation(user, FriendRequestStatus.RECEIVED) + + return AddFriendResponse(friend.id!!) } override fun deleteFriend( @@ -26,4 +42,22 @@ class FriendServiceImpl() : FriendService { ): DeleteFriendResponse { TODO("Not yet implemented") } + + private fun checkSelfFriend( + user: User, + friend: User, + ) { + if (user == friend) { + throw ServiceException(UserErrorCode.NOT_SELF_FOLLOW) + } + } + + private fun checkFriendRelationExists( + userId: Long, + friendId: Long, + ) { + if (friendRepository.existsByUserIdAndFriendId(userId, friendId)) { + throw ServiceException(UserErrorCode.ALREADY_FRIEND) + } + } } diff --git a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt index ee5e8011..4f467467 100644 --- a/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/UserServiceImpl.kt @@ -89,7 +89,7 @@ class UserServiceImpl( } } - private fun getUser(userId: Long): User { + fun getUser(userId: Long): User { return userRepository.findById(userId) .orElseThrow { throw ServiceException(UserErrorCode.USER_NOT_FOUND) } } From abc4fa484a5881f94fffc39917b8511a1b294dd1 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 19:00:28 +0900 Subject: [PATCH 093/178] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20mongoAuditing=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chat/src/main/resources/application.yml b/chat/src/main/resources/application.yml index 23bfa40b..e009b407 100644 --- a/chat/src/main/resources/application.yml +++ b/chat/src/main/resources/application.yml @@ -1,6 +1,8 @@ spring: application: name: chat + main: + allow-bean-definition-overriding: true data: mongodb: host: localhost From 0ed93d78b62fecafa6b78ca2abd78b5a8c0b81df Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 19:00:52 +0900 Subject: [PATCH 094/178] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20mongoAuditing=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20TestMongoCon?= =?UTF-8?q?fig=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/global/config/TestMongoConfig.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 chat/src/test/kotlin/kpring/chat/global/config/TestMongoConfig.kt diff --git a/chat/src/test/kotlin/kpring/chat/global/config/TestMongoConfig.kt b/chat/src/test/kotlin/kpring/chat/global/config/TestMongoConfig.kt new file mode 100644 index 00000000..3bc3120e --- /dev/null +++ b/chat/src/test/kotlin/kpring/chat/global/config/TestMongoConfig.kt @@ -0,0 +1,15 @@ +package kpring.chat.global.config + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.data.mongodb.config.EnableMongoAuditing +import org.springframework.data.mongodb.core.mapping.MongoMappingContext + +@TestConfiguration +@EnableMongoAuditing +class TestMongoConfig { + @Bean + fun mongoMappingContext(): MongoMappingContext { + return MongoMappingContext() + } +} From 51275ccc0e08484e40bd2e54624fbb5be4cd1cb4 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 19:01:15 +0900 Subject: [PATCH 095/178] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20mongoAuditing=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20TestMongoCon?= =?UTF-8?q?fig=20ChatControllerTest=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt index 7d36fa65..20fed416 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt @@ -8,6 +8,7 @@ import io.mockk.junit5.MockKExtension import kpring.chat.chat.service.ChatService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest +import kpring.chat.global.config.TestMongoConfig import kpring.core.auth.client.AuthClient import kpring.core.auth.dto.response.TokenInfo import kpring.core.auth.enums.TokenType @@ -23,6 +24,7 @@ import kpring.test.restdoc.json.JsonDataType import kpring.test.web.URLBuilder import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import import org.springframework.http.ResponseEntity import org.springframework.restdocs.ManualRestDocumentation import org.springframework.restdocs.RestDocumentationExtension @@ -38,6 +40,7 @@ import java.time.LocalDateTime @ExtendWith(RestDocumentationExtension::class) @ExtendWith(SpringExtension::class) @ExtendWith(MockKExtension::class) +@Import(TestMongoConfig::class) class ChatControllerTest( private val om: ObjectMapper, webContext: WebApplicationContext, From ee82aa101625ff39c09277d862d3ce0941fa358d Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 19:13:21 +0900 Subject: [PATCH 096/178] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9=EB=90=9C=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/resources/application.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/chat/src/main/resources/application.yml b/chat/src/main/resources/application.yml index 03d28262..c5bb0569 100644 --- a/chat/src/main/resources/application.yml +++ b/chat/src/main/resources/application.yml @@ -20,11 +20,6 @@ spring: port: 6379 password: testpassword1234 - redis: - host: localhost - port: 6379 - password: testpassword1234 - server: port: 8081 From d91e903980c8c6806cc1e588b742657659866a53 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 10 Jun 2024 19:13:48 +0900 Subject: [PATCH 097/178] =?UTF-8?q?feat:=20ChatRoomControllerTest=EC=97=90?= =?UTF-8?q?=20MongoConfig=20Import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt index 90ba6067..e157c620 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -9,6 +9,7 @@ import kpring.chat.chatroom.api.v1.ChatRoomController import kpring.chat.chatroom.service.ChatRoomService import kpring.chat.global.ChatRoomTest import kpring.chat.global.CommonTest +import kpring.chat.global.config.TestMongoConfig import kpring.core.auth.client.AuthClient import kpring.core.auth.dto.response.TokenInfo import kpring.core.auth.enums.TokenType @@ -18,6 +19,7 @@ import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import import org.springframework.restdocs.ManualRestDocumentation import org.springframework.restdocs.RestDocumentationExtension import org.springframework.restdocs.operation.preprocess.Preprocessors @@ -31,6 +33,7 @@ import org.springframework.web.context.WebApplicationContext @ExtendWith(RestDocumentationExtension::class) @ExtendWith(SpringExtension::class) @ExtendWith(MockKExtension::class) +@Import(TestMongoConfig::class) class ChatRoomControllerTest( private val om: ObjectMapper, webContext: WebApplicationContext, From a4c6128b4ee53cbcc2736a5cc7fa8678d2552728 Mon Sep 17 00:00:00 2001 From: minahYu Date: Mon, 10 Jun 2024 21:34:09 +0900 Subject: [PATCH 098/178] =?UTF-8?q?test:=20AuthValidator=20=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerTest.kt | 48 ++++++++++--------- .../user/service/UserServiceImplTest.kt | 40 ---------------- 2 files changed, 25 insertions(+), 63 deletions(-) diff --git a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt index 41398594..5cd805dd 100644 --- a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt @@ -6,11 +6,7 @@ import io.kotest.core.spec.style.DescribeSpec import io.mockk.clearMocks import io.mockk.every import io.mockk.junit5.MockKExtension -import io.mockk.verify import kpring.core.auth.client.AuthClient -import kpring.core.auth.dto.response.TokenInfo -import kpring.core.auth.dto.response.TokenValidationResponse -import kpring.core.auth.enums.TokenType import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc @@ -22,6 +18,7 @@ import kpring.user.dto.response.FailMessageResponse import kpring.user.dto.response.GetUserProfileResponse import kpring.user.dto.response.UpdateUserProfileResponse import kpring.user.exception.UserErrorCode +import kpring.user.global.AuthValidator import kpring.user.service.UserService import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @@ -47,6 +44,7 @@ class UserControllerTest( webContext: WebApplicationContext, @MockkBean val authClient: AuthClient, @MockkBean val userService: UserService, + @MockkBean val authValidator: AuthValidator, ) : DescribeSpec( { @@ -298,9 +296,8 @@ class UserControllerTest( val requestJson = objectMapper.writeValueAsString(request) val response = ApiResponse(data = data) - every { authClient.getTokenInfo(any()) }.returns( - ApiResponse(data = TokenInfo(TokenType.ACCESS, userId.toString())), - ) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() + every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit every { userService.updateProfile(userId, any(), any()) } returns data val bodyBuilder = MultipartBodyBuilder() @@ -385,7 +382,10 @@ class UserControllerTest( val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() - every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws + ServiceException( + UserErrorCode.NOT_ALLOWED, + ) val bodyBuilder = MultipartBodyBuilder() @@ -467,7 +467,7 @@ class UserControllerTest( val requestJson = objectMapper.writeValueAsString(request) val response = FailMessageResponse.serverError - every { authClient.getTokenInfo(token) } throws RuntimeException("서버 내부 오류") + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws RuntimeException("서버 내부 오류") val bodyBuilder = MultipartBodyBuilder() @@ -538,9 +538,7 @@ class UserControllerTest( .username(TEST_USERNAME) .build() val response = ApiResponse(data = data) - every { authClient.getTokenInfo(token) }.returns( - ApiResponse(data = TokenInfo(TokenType.ACCESS, userId.toString())), - ) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() every { userService.getProfile(userId) } returns data // when @@ -586,7 +584,10 @@ class UserControllerTest( val token = "Bearer test" val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() - every { authClient.getTokenInfo(token) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws + ServiceException( + UserErrorCode.NOT_ALLOWED, + ) // when val result = @@ -621,7 +622,9 @@ class UserControllerTest( // given val userId = 1L val token = "Bearer test" - every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } throws RuntimeException("서버 내부 오류") val response = FailMessageResponse.serverError // when @@ -656,9 +659,8 @@ class UserControllerTest( it("탈퇴 성공") { // given val userId = 1L - val validationResponse = TokenValidationResponse(true, TokenType.ACCESS, userId.toString()) - every { authClient.getTokenInfo(any()) } returns - ApiResponse(data = TokenInfo(TokenType.ACCESS, userId.toString())) + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() + every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit every { userService.exitUser(userId) } returns true // when @@ -669,7 +671,6 @@ class UserControllerTest( .exchange() // then - verify(exactly = 1) { authClient.getTokenInfo(any()) } val docsRoot = result .expectStatus().isOk @@ -693,8 +694,9 @@ class UserControllerTest( it("탈퇴 실패 : 권한이 없는 토큰") { // given val userId = 1L - val validationResponse = TokenValidationResponse(false, null, null) - every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } throws ServiceException(UserErrorCode.NOT_ALLOWED) // when val result = @@ -704,7 +706,6 @@ class UserControllerTest( .exchange() // then - verify(exactly = 1) { authClient.getTokenInfo(any()) } val docsRoot = result .expectStatus().isForbidden @@ -727,7 +728,9 @@ class UserControllerTest( // given val userId = 1L val token = "Bearer token" - every { authClient.getTokenInfo(token) } throws RuntimeException("서버 내부 오류") + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } throws RuntimeException("서버 내부 오류") // when val result = @@ -737,7 +740,6 @@ class UserControllerTest( .exchange() // then - verify(exactly = 1) { authClient.getTokenInfo(any()) } val docsRoot = result .expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt index 35ecc9ee..ec4c34ca 100644 --- a/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/UserServiceImplTest.kt @@ -24,7 +24,6 @@ class UserServiceImplTest : FunSpec({ userValidationService, uploadProfileImageService, ) - val friendService = FriendService(userRepository) lateinit var createUserRequest: CreateUserRequest beforeTest { @@ -76,45 +75,6 @@ class UserServiceImplTest : FunSpec({ verify { userRepository.save(any()) wasNot Called } } - -// test("친구추가_성공") { -// val user = User(id = 1L, username = "user1", followers = mutableSetOf(), followees = mutableSetOf()) -// val friend = User(id = 2L, username = "user2", followers = mutableSetOf(), followees = mutableSetOf()) -// -// `when`(userRepository.findById(user.id!!)).thenReturn(Optional.of(user)) -// `when`(userRepository.findById(friend.id!!)).thenReturn(Optional.of(friend)) -// -// val result: AddFriendResponse = friendService.addFriend(AddFriendRequest(friend.id!!), user.id) -// -// assertEquals(friend.id!!, result.friendId) -// -// // Verify that the followers and followees relationships are updated -// user.followers.forEach { follower -> -// assertEquals(1, follower.followees.size) -// assertEquals(user, follower.followees.first()) -// } -// user.followees.forEach { followee -> -// assertEquals(1, followee.followers.size) -// assertEquals(user, followee.followers.first()) -// } -// -// verify(userRepository).findById(user.id!!) -// } -// -// test("친구추가실패_유저조회불가능케이스") { -// val userId = 2L -// val friendId = 1L -// -// `when`(userRepository.findById(userId)).thenReturn(Optional.empty()) -// -// val exception = assertThrows(IllegalArgumentException::class.java) { -// friendService.addFriend(AddFriendRequest(friendId), userId) -// } -// -// assertEquals("User not found", exception.message) -// -// verify(userRepository).findById(userId) -// } }) { companion object { private const val TEST_EMAIL = "test@email.com" From 2515402d8a3d24ce9f339e06cc721ba2117c856b Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 09:27:04 +0900 Subject: [PATCH 099/178] =?UTF-8?q?feat=20:=20category=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/core/server/dto/CategoryInfo.kt | 5 ++++ .../adapter/input/rest/CategoryController.kt | 23 +++++++++++++++++++ .../input/rest/CategoryControllerTest.kt | 6 +++++ 3 files changed, 34 insertions(+) create mode 100644 core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt create mode 100644 server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt create mode 100644 server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt diff --git a/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt b/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt new file mode 100644 index 00000000..714e308b --- /dev/null +++ b/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt @@ -0,0 +1,5 @@ +package kpring.core.server.dto + +data class CategoryInfo( + val name: String, +) diff --git a/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt b/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt new file mode 100644 index 00000000..bdd57ce8 --- /dev/null +++ b/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt @@ -0,0 +1,23 @@ +package kpring.server.adapter.input.rest + +import kpring.core.global.dto.response.ApiResponse +import kpring.core.server.dto.CategoryInfo +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/category") +class CategoryController() { + @GetMapping("") + fun getCategories(): ResponseEntity> { + val response = + listOf( + CategoryInfo("카테고리1"), + CategoryInfo("카테고리2"), + ) + return ResponseEntity.ok() + .body(ApiResponse(data = response)) + } +} diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt new file mode 100644 index 00000000..3a7f2568 --- /dev/null +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt @@ -0,0 +1,6 @@ +package kpring.server.adapter.input.rest + +import io.kotest.core.spec.style.DescribeSpec + +class CategoryControllerTest : DescribeSpec({ +}) From b4d72613e084738c84634a9008fd2c3e835ab991 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 09:52:37 +0900 Subject: [PATCH 100/178] =?UTF-8?q?feat=20:=20WebTestClient=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B3=B5=ED=86=B5=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/rest/RestApiServerControllerTest.kt | 598 +++++++++--------- .../test/web/WebTestClientDescribeSpec.kt | 34 + 2 files changed, 325 insertions(+), 307 deletions(-) create mode 100644 test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index 741cca7d..fc8e6ae3 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -2,7 +2,6 @@ package kpring.server.adapter.input.rest import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean -import io.kotest.core.spec.style.DescribeSpec import io.mockk.clearMocks import io.mockk.every import io.mockk.junit5.MockKExtension @@ -24,13 +23,10 @@ import kpring.server.config.CoreConfiguration import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType.* import kpring.test.web.URLBuilder +import kpring.test.web.WebTestClientDescribeSpec import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Import -import org.springframework.restdocs.ManualRestDocumentation -import org.springframework.restdocs.operation.preprocess.Preprocessors -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext @Import(CoreConfiguration::class) @@ -45,338 +41,326 @@ class RestApiServerControllerTest( webContext: WebApplicationContext, @MockkBean val serverService: ServerService, @MockkBean val authClient: AuthClient, -) : DescribeSpec({ - - val restDocument = ManualRestDocumentation() - val webTestClient = - MockMvcWebTestClient.bindToApplicationContext(webContext) - .configureClient() - .filter( - WebTestClientRestDocumentation.documentationConfiguration(restDocument) - .operationPreprocessors() - .withRequestDefaults(Preprocessors.prettyPrint()) - .withResponseDefaults(Preprocessors.prettyPrint()), - ) - .build() - - beforeSpec { restDocument.beforeTest(this.javaClass, "user controller") } - - afterSpec { restDocument.afterTest() } - - afterTest { clearMocks(authClient) } - - describe("POST /api/v1/server : createServer api test") { - val url = "/api/v1/server" - it("요청 성공시") { - // given - val request = CreateServerRequest(serverName = "test server") - val data = CreateServerResponse(serverId = "1", serverName = request.serverName) - - every { authClient.getTokenInfo(any()) } returns - ApiResponse( - data = - TokenInfo( - type = TokenType.ACCESS, - userId = "test_user_id", - ), - ) - every { serverService.createServer(eq(request), any()) } returns data - - // when - val result = - webTestClient.post() - .uri(url) - .header("Authorization", "Bearer mock_token") - .bodyValue(request) - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - .json(om.writeValueAsString(ApiResponse(data = data))) - - // docs - docs.restDoc( - identifier = "create_server_200", - description = "서버 생성 api", - ) { - request { - header { "Authorization" mean "jwt access token" } - body { - "serverName" type Strings mean "생성할 서버의 이름" +) : WebTestClientDescribeSpec( + testMethodName = "RestApiServerControllerTest", + webContext = webContext, + body = { client -> + + afterTest { clearMocks(authClient) } + + describe("POST /api/v1/server : createServer api test") { + val url = "/api/v1/server" + it("요청 성공시") { + // given + val request = CreateServerRequest(serverName = "test server") + val data = CreateServerResponse(serverId = "1", serverName = request.serverName) + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, + userId = "test_user_id", + ), + ) + every { serverService.createServer(eq(request), any()) } returns data + + // when + val result = + client.post() + .uri(url) + .header("Authorization", "Bearer mock_token") + .bodyValue(request) + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(data = data))) + + // docs + docs.restDoc( + identifier = "create_server_200", + description = "서버 생성 api", + ) { + request { + header { "Authorization" mean "jwt access token" } + body { + "serverName" type Strings mean "생성할 서버의 이름" + } } - } - response { - body { - "data.serverId" type Strings mean "서버 id" - "data.serverName" type Strings mean "생성된 서버 이름" + response { + body { + "data.serverId" type Strings mean "서버 id" + "data.serverName" type Strings mean "생성된 서버 이름" + } } } } } - } - - describe("GET /api/v1/server/{serverId}: 서버 조회 api test") { - - val url = "/api/v1/server/{serverId}" - it("요청 성공시") { - // given - val serverId = "test_server_id" - val data = ServerInfo(id = serverId, name = "test_server", users = emptyList()) - every { serverService.getServerInfo(serverId) } returns data - - // when - val result = - webTestClient.get() - .uri(url, serverId) - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - .json(om.writeValueAsString(ApiResponse(data = data))) - - // docs - docs.restDoc( - identifier = "get_server_info_200", - description = "서버 단건 조회 api", - ) { - request { - path { "serverId" mean "서버 id" } - } - response { - body { - "data.id" type Strings mean "서버 id" - "data.name" type Strings mean "생성된 서버 이름" - "data.users" type Arrays mean "서버에 가입된 유저 목록" + describe("GET /api/v1/server/{serverId}: 서버 조회 api test") { + + val url = "/api/v1/server/{serverId}" + it("요청 성공시") { + // given + val serverId = "test_server_id" + val data = ServerInfo(id = serverId, name = "test_server", users = emptyList()) + every { serverService.getServerInfo(serverId) } returns data + + // when + val result = + client.get() + .uri(url, serverId) + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(data = data))) + + // docs + docs.restDoc( + identifier = "get_server_info_200", + description = "서버 단건 조회 api", + ) { + request { + path { "serverId" mean "서버 id" } + } + + response { + body { + "data.id" type Strings mean "서버 id" + "data.name" type Strings mean "생성된 서버 이름" + "data.users" type Arrays mean "서버에 가입된 유저 목록" + } } } } - } - it("요청 실패 : 존재하지 않은 서버") { - // given - val serverId = "not_exist_server_id" - val errorCode = CommonErrorCode.NOT_FOUND - every { serverService.getServerInfo(serverId) } throws ServiceException(errorCode) - - // when - val result = - webTestClient.get() - .uri(url, serverId) - .exchange() - - // then - val docs = - result - .expectStatus().isNotFound - .expectBody() - .json(om.writeValueAsString(ApiResponse(message = errorCode.message()))) - - // docs - docs.restDoc( - identifier = "get_server_info_with_invalid_id", - description = "서버 단건 조회 api", - ) { - request { - path { "serverId" mean "서버 id" } - } + it("요청 실패 : 존재하지 않은 서버") { + // given + val serverId = "not_exist_server_id" + val errorCode = CommonErrorCode.NOT_FOUND + every { serverService.getServerInfo(serverId) } throws ServiceException(errorCode) + + // when + val result = + client.get() + .uri(url, serverId) + .exchange() + + // then + val docs = + result + .expectStatus().isNotFound + .expectBody() + .json(om.writeValueAsString(ApiResponse(message = errorCode.message()))) + + // docs + docs.restDoc( + identifier = "get_server_info_with_invalid_id", + description = "서버 단건 조회 api", + ) { + request { + path { "serverId" mean "서버 id" } + } - response { - body { - "message" type Strings mean "실패 관련 메시지" + response { + body { + "message" type Strings mean "실패 관련 메시지" + } } } } } - } - - describe("PUT /api/v1/server/{serverId}/user : 서버 가입 api test") { - val url = "/api/v1/server/{serverId}/user" - it("요청 성공시") { - // given - val request = - AddUserAtServerRequest(userId = "userId", userName = "test", profileImage = "test") - justRun { serverService.addInvitedUser("test_server_id", request) } - - // when - val result = - webTestClient.put() - .uri(url, "test_server_id") - .header("Authorization", "Bearer mock_token") - .bodyValue(request) - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - - // docs - docs.restDoc( - identifier = "add_invited_user_200", - description = "서버 가입 api", - ) { - request { - header { "Authorization" mean "jwt access token" } - path { "serverId" mean "서버 id" } - body { - "userId" type Strings mean "가입할 유저 id" - "userName" type Strings mean "가입할 유저 이름" - "profileImage" type Strings mean "가입할 유저 프로필 이미지" + + describe("PUT /api/v1/server/{serverId}/user : 서버 가입 api test") { + val url = "/api/v1/server/{serverId}/user" + it("요청 성공시") { + // given + val request = + AddUserAtServerRequest(userId = "userId", userName = "test", profileImage = "test") + justRun { serverService.addInvitedUser("test_server_id", request) } + + // when + val result = + client.put() + .uri(url, "test_server_id") + .header("Authorization", "Bearer mock_token") + .bodyValue(request) + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + + // docs + docs.restDoc( + identifier = "add_invited_user_200", + description = "서버 가입 api", + ) { + request { + header { "Authorization" mean "jwt access token" } + path { "serverId" mean "서버 id" } + body { + "userId" type Strings mean "가입할 유저 id" + "userName" type Strings mean "가입할 유저 이름" + "profileImage" type Strings mean "가입할 유저 프로필 이미지" + } } } } } - } - - describe("PUT /api/v1/server/{serverId}/invitation/{userId} : 서버 초대 api test") { - val url = "/api/v1/server/{serverId}/invitation/{userId}" - it("요청 성공시") { - // given - every { authClient.getTokenInfo(any()) } returns - ApiResponse( - data = - TokenInfo( - type = TokenType.ACCESS, - userId = "test_user_id", - ), - ) - justRun { serverService.inviteUser(eq("test_server_id"), any(), any()) } - - // when - val result = - webTestClient.put() - .uri(url, "test_server_id", "userId") - .header("Authorization", "Bearer mock_token") - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - - // docs - docs.restDoc( - identifier = "invite_user_200", - description = "서버 초대 api", - ) { - request { - header { "Authorization" mean "jwt access token" } - path { - "serverId" mean "서버 id" - "userId" mean "초대할 유저 id" + + describe("PUT /api/v1/server/{serverId}/invitation/{userId} : 서버 초대 api test") { + val url = "/api/v1/server/{serverId}/invitation/{userId}" + it("요청 성공시") { + // given + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, + userId = "test_user_id", + ), + ) + justRun { serverService.inviteUser(eq("test_server_id"), any(), any()) } + + // when + val result = + client.put() + .uri(url, "test_server_id", "userId") + .header("Authorization", "Bearer mock_token") + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + + // docs + docs.restDoc( + identifier = "invite_user_200", + description = "서버 초대 api", + ) { + request { + header { "Authorization" mean "jwt access token" } + path { + "serverId" mean "서버 id" + "userId" mean "초대할 유저 id" + } } } } } - } - - describe("GET /api/v1/server: 서버 목록 조회 api test") { - - val url = "/api/v1/server" - it("요청 성공시") { - // given - val userId = "test user id" - val data = - listOf( - ServerSimpleInfo(id = "server1", name = "test_server", bookmarked = false), - ServerSimpleInfo(id = "server2", name = "test_server", bookmarked = true), - ) - val condition = GetServerCondition(serverIds = listOf("server1", "server2")) - - every { authClient.getTokenInfo(any()) } returns - ApiResponse( - data = TokenInfo(TokenType.ACCESS, userId), - ) - every { serverService.getServerList(any(), eq(userId)) } returns data - - // when - val result = - webTestClient.get() - .uri( - URLBuilder(url) - .query("serverIds", condition.serverIds!!) - .build(), + + describe("GET /api/v1/server: 서버 목록 조회 api test") { + + val url = "/api/v1/server" + it("요청 성공시") { + // given + val userId = "test user id" + val data = + listOf( + ServerSimpleInfo(id = "server1", name = "test_server", bookmarked = false), + ServerSimpleInfo(id = "server2", name = "test_server", bookmarked = true), ) - .header("Authorization", "Bearer test_token") - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - .json(om.writeValueAsString(ApiResponse(data = data))) - - // docs - docs.restDoc( - identifier = "get_server_list_info_200", - description = "서버 목록 조회 api", - ) { - request { - query { "serverIds" mean "조회시 해당 서버 목록만을 조회합니다. 값이 없다면 조건은 적용되지 않습니다." } - header { "Authorization" mean "jwt access token" } - } + val condition = GetServerCondition(serverIds = listOf("server1", "server2")) + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = TokenInfo(TokenType.ACCESS, userId), + ) + every { serverService.getServerList(any(), eq(userId)) } returns data + + // when + val result = + client.get() + .uri( + URLBuilder(url) + .query("serverIds", condition.serverIds!!) + .build(), + ) + .header("Authorization", "Bearer test_token") + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(data = data))) + + // docs + docs.restDoc( + identifier = "get_server_list_info_200", + description = "서버 목록 조회 api", + ) { + request { + query { "serverIds" mean "조회시 해당 서버 목록만을 조회합니다. 값이 없다면 조건은 적용되지 않습니다." } + header { "Authorization" mean "jwt access token" } + } - response { - body { - "data[].id" type Strings mean "서버 id" - "data[].name" type Strings mean "서버 이름" - "data[].bookmarked" type Booleans mean "북마크 여부" + response { + body { + "data[].id" type Strings mean "서버 id" + "data[].name" type Strings mean "서버 이름" + "data[].bookmarked" type Booleans mean "북마크 여부" + } } } } } - } - - describe("DELETE /api/v1/server/{serverId} : 서버 삭제") { - val url = "/api/v1/server/{serverId}" - it("요청 성공시") { - // given - val serverId = "test_server_id" - val token = "Bearer mock_token" - every { authClient.getTokenInfo(token) } returns - ApiResponse( - data = - TokenInfo( - type = TokenType.ACCESS, - userId = "test_user_id", - ), - ) - justRun { serverService.deleteServer(eq(serverId), any()) } - - // when - val result = - webTestClient.delete() - .uri(url, serverId) - .header("Authorization", token) - .exchange() - - // then - val docs = - result - .expectStatus().isOk - .expectBody() - - // docs - docs.restDoc( - identifier = "delete_server_200", - description = "서버 삭제 api", - ) { - request { - header { "Authorization" mean "jwt 사용자 토큰" } - path { "serverId" mean "서버 id" } + + describe("DELETE /api/v1/server/{serverId} : 서버 삭제") { + val url = "/api/v1/server/{serverId}" + it("요청 성공시") { + // given + val serverId = "test_server_id" + val token = "Bearer mock_token" + every { authClient.getTokenInfo(token) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, + userId = "test_user_id", + ), + ) + justRun { serverService.deleteServer(eq(serverId), any()) } + + // when + val result = + client.delete() + .uri(url, serverId) + .header("Authorization", token) + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody() + + // docs + docs.restDoc( + identifier = "delete_server_200", + description = "서버 삭제 api", + ) { + request { + header { "Authorization" mean "jwt 사용자 토큰" } + path { "serverId" mean "서버 id" } + } } } } - } - }) + }, + ) diff --git a/test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt b/test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt new file mode 100644 index 00000000..18f438e6 --- /dev/null +++ b/test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt @@ -0,0 +1,34 @@ +package kpring.test.web + +import io.kotest.core.spec.style.DescribeSpec +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.operation.preprocess.Preprocessors +import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.client.MockMvcWebTestClient +import org.springframework.web.context.WebApplicationContext + +open class WebTestClientDescribeSpec( + testMethodName: String, + webContext: WebApplicationContext, + body: DescribeSpec.(webTestClient: WebTestClient) -> Unit = {}, +) : DescribeSpec({ + + val restDocument = ManualRestDocumentation() + + val webTestClient: WebTestClient = + MockMvcWebTestClient.bindToApplicationContext(webContext) + .configureClient() + .filter( + WebTestClientRestDocumentation.documentationConfiguration(restDocument) + .operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()) + .withResponseDefaults(Preprocessors.prettyPrint()), + ) + .build() + + beforeSpec { restDocument.beforeTest(this.javaClass, testMethodName) } + afterSpec { restDocument.afterTest() } + + body(webTestClient) + }) From 14d85f2def18446b1b492444c063ab98c2cb0dfe Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 09:56:34 +0900 Subject: [PATCH 101/178] =?UTF-8?q?fix=20:=20Mvc=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/adapter/input/rest/RestApiServerControllerTest.kt | 4 ++-- ...tClientDescribeSpec.kt => MvcWebTestClientDescribeSpec.kt} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename test/src/main/kotlin/kpring/test/web/{WebTestClientDescribeSpec.kt => MvcWebTestClientDescribeSpec.kt} (96%) diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index fc8e6ae3..d484e3b1 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -22,8 +22,8 @@ import kpring.server.application.service.ServerService import kpring.server.config.CoreConfiguration import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType.* +import kpring.test.web.MvcWebTestClientDescribeSpec import kpring.test.web.URLBuilder -import kpring.test.web.WebTestClientDescribeSpec import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Import @@ -41,7 +41,7 @@ class RestApiServerControllerTest( webContext: WebApplicationContext, @MockkBean val serverService: ServerService, @MockkBean val authClient: AuthClient, -) : WebTestClientDescribeSpec( +) : MvcWebTestClientDescribeSpec( testMethodName = "RestApiServerControllerTest", webContext = webContext, body = { client -> diff --git a/test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt b/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt similarity index 96% rename from test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt rename to test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt index 18f438e6..2d6a1732 100644 --- a/test/src/main/kotlin/kpring/test/web/WebTestClientDescribeSpec.kt +++ b/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt @@ -8,7 +8,7 @@ import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext -open class WebTestClientDescribeSpec( +open class MvcWebTestClientDescribeSpec( testMethodName: String, webContext: WebApplicationContext, body: DescribeSpec.(webTestClient: WebTestClient) -> Unit = {}, From 3c6a94ad89ba939982276ccf8d23f8112d26cb54 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 10:05:06 +0900 Subject: [PATCH 102/178] =?UTF-8?q?test=20:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20api=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/rest/CategoryControllerTest.kt | 68 ++++++++++++++++++- .../input/rest/RestApiServerControllerTest.kt | 7 +- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt index 3a7f2568..f50542f8 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt @@ -1,6 +1,68 @@ package kpring.server.adapter.input.rest -import io.kotest.core.spec.style.DescribeSpec +import com.fasterxml.jackson.databind.ObjectMapper +import com.ninjasquad.springmockk.MockkBean +import io.mockk.junit5.MockKExtension +import kpring.core.auth.client.AuthClient +import kpring.core.global.dto.response.ApiResponse +import kpring.core.server.dto.CategoryInfo +import kpring.server.application.service.ServerService +import kpring.server.config.CoreConfiguration +import kpring.test.restdoc.dsl.restDoc +import kpring.test.web.MvcWebTestClientDescribeSpec +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.web.context.WebApplicationContext -class CategoryControllerTest : DescribeSpec({ -}) +@Import(CoreConfiguration::class) +@WebMvcTest( + controllers = [ + RestApiServerController::class, + CategoryController::class, + ], +) +@ExtendWith( + value = [ + MockKExtension::class, + ], +) +class CategoryControllerTest( + private val om: ObjectMapper, + webContext: WebApplicationContext, + @MockkBean val serverService: ServerService, + @MockkBean val authClient: AuthClient, +) : MvcWebTestClientDescribeSpec( + "Rest api server category controller test", + webContext, + { client -> + describe("GET /api/v1/category") { + it("200 : 성공 케이스") { + // given + val data = + listOf( + CategoryInfo("카테고리1"), + CategoryInfo("카테고리2"), + ) + // when + val result = + client.get() + .uri("/api/v1/category") + .exchange() + + // then + val docs = + result + .expectStatus().isOk + .expectBody().json(om.writeValueAsString(ApiResponse(data = data))) + + // docs + docs.restDoc( + identifier = "get-categories", + description = "서버 카테고리 목록 조회", + ) { + } + } + } + }, + ) diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index d484e3b1..026f820f 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -30,7 +30,12 @@ import org.springframework.context.annotation.Import import org.springframework.web.context.WebApplicationContext @Import(CoreConfiguration::class) -@WebMvcTest(controllers = [RestApiServerController::class]) +@WebMvcTest( + controllers = [ + RestApiServerController::class, + CategoryController::class, + ], +) @ExtendWith( value = [ MockKExtension::class, From 52522890c71afb9ca4b1d404c9851bcb325e89dc Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 10:11:07 +0900 Subject: [PATCH 103/178] =?UTF-8?q?feat=20:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20domain=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/server/domain/Category.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 server/src/main/kotlin/kpring/server/domain/Category.kt diff --git a/server/src/main/kotlin/kpring/server/domain/Category.kt b/server/src/main/kotlin/kpring/server/domain/Category.kt new file mode 100644 index 00000000..eb46dc83 --- /dev/null +++ b/server/src/main/kotlin/kpring/server/domain/Category.kt @@ -0,0 +1,13 @@ +package kpring.server.domain + +enum class Category( + private val toString: String, +) { + SERVER_CATEGORY1("학습"), + SERVER_CATEGORY2("운동"), + ; + + override fun toString(): String { + return toString + } +} From df0eee37771401b645a3c861b3bc5c7c20326321 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 10:25:03 +0900 Subject: [PATCH 104/178] =?UTF-8?q?feat=20:=20=EC=B9=B4=ED=85=8C=EC=BD=94?= =?UTF-8?q?=EB=A6=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/core/server/dto/CategoryInfo.kt | 1 + .../adapter/input/rest/CategoryController.kt | 12 ++++----- .../port/input/GetCategoryUseCase.kt | 7 ++++++ .../application/service/CategoryService.kt | 18 +++++++++++++ .../kotlin/kpring/server/domain/Category.kt | 4 +++ .../input/rest/CategoryControllerTest.kt | 14 +++++++++-- .../service/CategoryServiceTest.kt | 25 +++++++++++++++++++ 7 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 server/src/main/kotlin/kpring/server/application/port/input/GetCategoryUseCase.kt create mode 100644 server/src/main/kotlin/kpring/server/application/service/CategoryService.kt create mode 100644 server/src/test/kotlin/kpring/server/application/service/CategoryServiceTest.kt diff --git a/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt b/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt index 714e308b..f1268634 100644 --- a/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt +++ b/core/src/main/kotlin/kpring/core/server/dto/CategoryInfo.kt @@ -1,5 +1,6 @@ package kpring.core.server.dto data class CategoryInfo( + val id: String, val name: String, ) diff --git a/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt b/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt index bdd57ce8..4aa9232e 100644 --- a/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt +++ b/server/src/main/kotlin/kpring/server/adapter/input/rest/CategoryController.kt @@ -1,7 +1,7 @@ package kpring.server.adapter.input.rest import kpring.core.global.dto.response.ApiResponse -import kpring.core.server.dto.CategoryInfo +import kpring.server.application.port.input.GetCategoryUseCase import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping @@ -9,14 +9,12 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/v1/category") -class CategoryController() { +class CategoryController( + val getCategoryUseCase: GetCategoryUseCase, +) { @GetMapping("") fun getCategories(): ResponseEntity> { - val response = - listOf( - CategoryInfo("카테고리1"), - CategoryInfo("카테고리2"), - ) + val response = getCategoryUseCase.getCategories() return ResponseEntity.ok() .body(ApiResponse(data = response)) } diff --git a/server/src/main/kotlin/kpring/server/application/port/input/GetCategoryUseCase.kt b/server/src/main/kotlin/kpring/server/application/port/input/GetCategoryUseCase.kt new file mode 100644 index 00000000..f2dd3a61 --- /dev/null +++ b/server/src/main/kotlin/kpring/server/application/port/input/GetCategoryUseCase.kt @@ -0,0 +1,7 @@ +package kpring.server.application.port.input + +import kpring.core.server.dto.CategoryInfo + +interface GetCategoryUseCase { + fun getCategories(): List +} diff --git a/server/src/main/kotlin/kpring/server/application/service/CategoryService.kt b/server/src/main/kotlin/kpring/server/application/service/CategoryService.kt new file mode 100644 index 00000000..9018fc67 --- /dev/null +++ b/server/src/main/kotlin/kpring/server/application/service/CategoryService.kt @@ -0,0 +1,18 @@ +package kpring.server.application.service + +import kpring.core.server.dto.CategoryInfo +import kpring.server.application.port.input.GetCategoryUseCase +import kpring.server.domain.Category +import org.springframework.stereotype.Service + +@Service +class CategoryService : GetCategoryUseCase { + override fun getCategories(): List { + return Category.entries.map { category -> + CategoryInfo( + id = category.name, + name = category.toString(), + ) + } + } +} diff --git a/server/src/main/kotlin/kpring/server/domain/Category.kt b/server/src/main/kotlin/kpring/server/domain/Category.kt index eb46dc83..be9ccc16 100644 --- a/server/src/main/kotlin/kpring/server/domain/Category.kt +++ b/server/src/main/kotlin/kpring/server/domain/Category.kt @@ -1,5 +1,9 @@ package kpring.server.domain +/** + * 2024-06-11 기준으로 카테고리와 관련된 기능이 없기 때문에 하드 코딩된 제한적인 카테고리를 사용합니다. + * 카테고리의 기능이 확장된다면 해당 클래스는 deprecated 처리되며 해당 정보는 DB에서 관리될 예정입니다. + */ enum class Category( private val toString: String, ) { diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt index f50542f8..9f394f82 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/CategoryControllerTest.kt @@ -2,10 +2,12 @@ package kpring.server.adapter.input.rest import com.fasterxml.jackson.databind.ObjectMapper import com.ninjasquad.springmockk.MockkBean +import io.mockk.every import io.mockk.junit5.MockKExtension import kpring.core.auth.client.AuthClient import kpring.core.global.dto.response.ApiResponse import kpring.core.server.dto.CategoryInfo +import kpring.server.application.service.CategoryService import kpring.server.application.service.ServerService import kpring.server.config.CoreConfiguration import kpring.test.restdoc.dsl.restDoc @@ -32,6 +34,7 @@ class CategoryControllerTest( webContext: WebApplicationContext, @MockkBean val serverService: ServerService, @MockkBean val authClient: AuthClient, + @MockkBean val categoryService: CategoryService, ) : MvcWebTestClientDescribeSpec( "Rest api server category controller test", webContext, @@ -41,9 +44,16 @@ class CategoryControllerTest( // given val data = listOf( - CategoryInfo("카테고리1"), - CategoryInfo("카테고리2"), + CategoryInfo( + id = "SERVER_CATEGORY_1", + name = "category1", + ), + CategoryInfo( + id = "SERVER_CATEGORY_2", + name = "category2", + ), ) + every { categoryService.getCategories() } returns data // when val result = client.get() diff --git a/server/src/test/kotlin/kpring/server/application/service/CategoryServiceTest.kt b/server/src/test/kotlin/kpring/server/application/service/CategoryServiceTest.kt new file mode 100644 index 00000000..fcf1981d --- /dev/null +++ b/server/src/test/kotlin/kpring/server/application/service/CategoryServiceTest.kt @@ -0,0 +1,25 @@ +package kpring.server.application.service + +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import kpring.core.server.dto.CategoryInfo +import kpring.server.domain.Category + +class CategoryServiceTest : FunSpec({ + + test("카테고리 목록 조회시 하드 코딩된 카테고리 정보를 조회한다.") { + // given + val categoryService = CategoryService() + + // when + val categories = categoryService.getCategories() + + // then + categories shouldBe + listOf( + CategoryInfo(id = Category.SERVER_CATEGORY1.name, name = Category.SERVER_CATEGORY1.toString()), + CategoryInfo(id = Category.SERVER_CATEGORY2.name, name = Category.SERVER_CATEGORY2.toString()), + ) + println(categories) + } +}) From 775e575fa601bec4ef1eadbe63c846a77a2a8065 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 11:58:14 +0900 Subject: [PATCH 105/178] =?UTF-8?q?fix=20:=20MvcWebTestClientDescribeSpec?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=96=89=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt b/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt index 2d6a1732..d7ec9f4f 100644 --- a/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt +++ b/test/src/main/kotlin/kpring/test/web/MvcWebTestClientDescribeSpec.kt @@ -8,7 +8,7 @@ import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.client.MockMvcWebTestClient import org.springframework.web.context.WebApplicationContext -open class MvcWebTestClientDescribeSpec( +abstract class MvcWebTestClientDescribeSpec( testMethodName: String, webContext: WebApplicationContext, body: DescribeSpec.(webTestClient: WebTestClient) -> Unit = {}, From 9b30e8501c2f3a84029aba1e112cd30a7b7bc704 Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 11 Jun 2024 13:01:03 +0900 Subject: [PATCH 106/178] =?UTF-8?q?refac:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=98=88=EC=99=B8=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/chat/chat/api/v1/ChatController.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt index 27361f4d..7aa81a6d 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt @@ -19,8 +19,8 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/api/v1") class ChatController( private val chatService: ChatService, - val authClient: AuthClient, - val serverClient: ServerClient, + private val authClient: AuthClient, + private val serverClient: ServerClient, ) { @PostMapping("/chat") fun createChat( @@ -71,7 +71,6 @@ class ChatController( when (request.type) { ChatType.Room -> chatService.updateRoomChat(request, userId) ChatType.Server -> chatService.updateServerChat(request, userId) - else -> throw GlobalException(ErrorCode.INVALID_CHAT_TYPE) } return ResponseEntity.ok().body(ApiResponse(status = 200)) } From ef2d895707889e478185bb59d4a1b47a4d1c0538 Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 11 Jun 2024 14:00:23 +0900 Subject: [PATCH 107/178] =?UTF-8?q?test:=20CommonTest=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/user/global/CommonTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 user/src/test/kotlin/kpring/user/global/CommonTest.kt diff --git a/user/src/test/kotlin/kpring/user/global/CommonTest.kt b/user/src/test/kotlin/kpring/user/global/CommonTest.kt new file mode 100644 index 00000000..80716bf7 --- /dev/null +++ b/user/src/test/kotlin/kpring/user/global/CommonTest.kt @@ -0,0 +1,14 @@ +package kpring.user.global + +interface CommonTest { + companion object { + const val TEST_USER_ID = 1L + const val TEST_EMAIL = "test@email.com" + const val TEST_PASSWORD = "Password123!" + const val TEST_ENCODED_PASSWORD = "EncodedPassword123!" + const val TEST_USERNAME = "test" + const val TEST_TOKEN = "Bearer test" + + const val TEST_FRIEND_ID = 2L + } +} From fce76eeb291c4d56a18ace4a932e6a77382cf5ed Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 11 Jun 2024 14:01:18 +0900 Subject: [PATCH 108/178] =?UTF-8?q?test:=20=EC=B9=9C=EA=B5=AC=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20controller=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/FriendControllerTest.kt | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt diff --git a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt new file mode 100644 index 00000000..bbe50206 --- /dev/null +++ b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt @@ -0,0 +1,194 @@ +package kpring.user.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import io.mockk.junit5.MockKExtension +import kpring.core.global.dto.response.ApiResponse +import kpring.core.global.exception.ServiceException +import kpring.test.restdoc.dsl.restDoc +import kpring.test.restdoc.json.JsonDataType.Strings +import kpring.user.dto.response.FailMessageResponse +import kpring.user.dto.result.AddFriendResponse +import kpring.user.exception.UserErrorCode +import kpring.user.global.AuthValidator +import kpring.user.global.CommonTest +import kpring.user.service.FriendServiceImpl +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.restdocs.ManualRestDocumentation +import org.springframework.restdocs.operation.preprocess.Preprocessors +import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.test.web.servlet.client.MockMvcWebTestClient +import org.springframework.web.context.WebApplicationContext + +@WebMvcTest(controllers = [FriendController::class]) +@ExtendWith(value = [MockKExtension::class]) +internal class FriendControllerTest( + private val objectMapper: ObjectMapper, + webContext: WebApplicationContext, + @MockkBean val friendService: FriendServiceImpl, + @MockkBean val authValidator: AuthValidator, +) : DescribeSpec({ + + val restDocument = ManualRestDocumentation() + val webTestClient = + MockMvcWebTestClient.bindToApplicationContext(webContext) + .configureClient() + .filter( + WebTestClientRestDocumentation.documentationConfiguration(restDocument) + .operationPreprocessors() + .withRequestDefaults(Preprocessors.prettyPrint()) + .withResponseDefaults(Preprocessors.prettyPrint()), + ) + .build() + + beforeSpec { restDocument.beforeTest(this.javaClass, "friend controller") } + + afterSpec { restDocument.afterTest() } + + describe("친구신청 API") { + it("친구신청 성공") { + // given + val data = AddFriendResponse(friendId = CommonTest.TEST_FRIEND_ID) + val response = ApiResponse(data = data) + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } returns CommonTest.TEST_USER_ID.toString() + every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit + every { + friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } returns data + + // when + val result = + webTestClient.post() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isOk + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "addFriend200", + description = "친구신청 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + "data.friendId" type Strings mean "친구신청을 받은 사용자의 아이디" + } + } + } + } + + it("친구신청 실패 : 권한이 없는 토큰") { + // given + val response = + FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() + every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws + ServiceException( + UserErrorCode.NOT_ALLOWED, + ) + + // when + val result = + webTestClient.post() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isForbidden + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "addFriend403", + description = "친구신청 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + body { "message" type Strings mean "에러 메시지" } + } + } + } + } + + it("친구신청 실패 : 서버 내부 오류") { + // given + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } throws RuntimeException("서버 내부 오류") + val response = FailMessageResponse.serverError + + // when + val result = + webTestClient.post() + .uri( + "/api/v1/user/{userId}/friend/{friendId}", + CommonTest.TEST_USER_ID, + CommonTest.TEST_FRIEND_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isEqualTo(500) + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "addFriend500", + description = "친구신청 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + "friendId" mean "친구신청을 받은 사용자의 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + "message" type Strings mean "에러 메시지" + } + } + } + } + } + }) From 12450fff1654a74d3ca777d395ec991f2b747290 Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 11 Jun 2024 14:01:49 +0900 Subject: [PATCH 109/178] =?UTF-8?q?test:=20=EC=B9=9C=EA=B5=AC=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20service=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/service/FriendServiceImpl.kt | 4 +- .../user/service/FriendServiceImplTest.kt | 102 ++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index bb79fdbc..f72099ea 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -43,7 +43,7 @@ class FriendServiceImpl( TODO("Not yet implemented") } - private fun checkSelfFriend( + fun checkSelfFriend( user: User, friend: User, ) { @@ -52,7 +52,7 @@ class FriendServiceImpl( } } - private fun checkFriendRelationExists( + fun checkFriendRelationExists( userId: Long, friendId: Long, ) { diff --git a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt new file mode 100644 index 00000000..fb22aa37 --- /dev/null +++ b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt @@ -0,0 +1,102 @@ +package kpring.user.service + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.mockk.* +import kpring.core.global.exception.ServiceException +import kpring.user.entity.FriendRequestStatus +import kpring.user.entity.User +import kpring.user.exception.UserErrorCode +import kpring.user.global.CommonTest +import kpring.user.repository.FriendRepository +import kpring.user.repository.UserRepository +import org.springframework.security.crypto.password.PasswordEncoder + +internal class FriendServiceImplTest : FunSpec({ + val userRepository: UserRepository = mockk() + val friendRepository: FriendRepository = mockk() + val passwordEncoder: PasswordEncoder = mockk() + val userValidationService: UserValidationService = mockk() + val uploadProfileImageService: UploadProfileImageService = mockk() + val userService = + UserServiceImpl( + userRepository, + passwordEncoder, + userValidationService, + uploadProfileImageService, + ) + val friendService = + FriendServiceImpl( + userService, + friendRepository, + ) + + test("친구신청_성공") { + val user = mockk(relaxed = true) + val friend = mockk(relaxed = true) + + every { userService.getUser(CommonTest.TEST_USER_ID) } returns user + every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend + + every { + friendRepository.existsByUserIdAndFriendId(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } returns false + every { user.addFriendRelation(friend, FriendRequestStatus.REQUESTED) } just Runs + every { friend.addFriendRelation(user, FriendRequestStatus.RECEIVED) } just Runs + + val response = friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + response.friendId shouldBe friend.id + + verify { friendService.checkSelfFriend(user, friend) } + verify { + friendService.checkFriendRelationExists(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } + } + + test("친구신청_실패_자기자신을 친구로 추가하는 케이스") { + val user = mockk(relaxed = true) + + every { userService.getUser(CommonTest.TEST_USER_ID) } returns user + every { + friendRepository.existsByUserIdAndFriendId(CommonTest.TEST_USER_ID, CommonTest.TEST_USER_ID) + } returns false + every { + friendService.checkSelfFriend(user, user) + } throws ServiceException(UserErrorCode.NOT_SELF_FOLLOW) + + val exception = + shouldThrow { + friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_USER_ID) + } + exception.errorCode.message() shouldBe "자기자신에게 친구요청을 보낼 수 없습니다" + + verify(exactly = 0) { user.addFriendRelation(any(), any()) } + } + + test("친구신청_실패_이미 친구인 케이스") { + val user = mockk(relaxed = true) + val friend = mockk(relaxed = true) + + every { userService.getUser(CommonTest.TEST_USER_ID) } returns user + every { userService.getUser(CommonTest.TEST_FRIEND_ID) } returns friend + + every { + friendRepository.existsByUserIdAndFriendId(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } returns true + every { + friendService.checkFriendRelationExists(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } throws ServiceException(UserErrorCode.ALREADY_FRIEND) + + val exception = + shouldThrow { + friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } + exception.errorCode.message() shouldBe "이미 친구입니다." + + verify { friendService.checkSelfFriend(user, friend) } + verify { + friendService.checkFriendRelationExists(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) + } + } +}) From 8c757101e15bcc80d0bdb5fec894b0c814f7cedd Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 11 Jun 2024 14:08:20 +0900 Subject: [PATCH 110/178] =?UTF-8?q?refac:=20verifyAccess=20Service=20Layer?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt index ffd0d432..dd11d349 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt @@ -1,8 +1,6 @@ package kpring.chat.chat.model import kpring.chat.NoArg -import kpring.chat.global.exception.ErrorCode -import kpring.chat.global.exception.GlobalException import kpring.chat.global.model.BaseTime import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @@ -25,10 +23,4 @@ class Chat( fun updateContent(content: String) { this.content = content } - - fun verifyAccess(userId: String) { - if (userId != this.userId) { - throw GlobalException(ErrorCode.FORBIDDEN_CHAT) - } - } } From 4957831549b26683233f5d4a04d51b6a9769879e Mon Sep 17 00:00:00 2001 From: minisun Date: Tue, 11 Jun 2024 14:08:51 +0900 Subject: [PATCH 111/178] =?UTF-8?q?feat:=20verifyIfAuthor=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/service/ChatService.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index 4a1c6402..aa0bbe2a 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -84,7 +84,7 @@ class ChatService( userId: String, ): Boolean { val chat = roomChatRepository.findById(request.id).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } - chat.verifyAccess(userId) + verifyIfAuthor(userId, chat) chat.updateContent(request.content) roomChatRepository.save(chat) return true @@ -95,12 +95,21 @@ class ChatService( userId: String, ): Boolean { val chat = serverChatRepository.findById(request.id).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } - chat.verifyAccess(userId) + verifyIfAuthor(userId, chat) chat.updateContent(request.content) serverChatRepository.save(chat) return true } + fun verifyIfAuthor( + userId: String, + chat: Chat, + ) { + if (userId != chat.userId) { + throw GlobalException(ErrorCode.FORBIDDEN_CHAT) + } + } + private fun verifyServerAccess( servers: List, serverId: String, From 0a043bd04a6d67fbadf3c937a0a9c857a627178c Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 11 Jun 2024 15:31:02 +0900 Subject: [PATCH 112/178] =?UTF-8?q?test=20:=20=EB=B9=88=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8B=A4=ED=8C=A8=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=A4=ED=8C=A8=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/adapter/input/rest/RestApiServerControllerTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index 026f820f..1c0898ee 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -18,6 +18,7 @@ import kpring.core.server.dto.request.AddUserAtServerRequest import kpring.core.server.dto.request.CreateServerRequest import kpring.core.server.dto.request.GetServerCondition import kpring.core.server.dto.response.CreateServerResponse +import kpring.server.application.service.CategoryService import kpring.server.application.service.ServerService import kpring.server.config.CoreConfiguration import kpring.test.restdoc.dsl.restDoc @@ -46,6 +47,7 @@ class RestApiServerControllerTest( webContext: WebApplicationContext, @MockkBean val serverService: ServerService, @MockkBean val authClient: AuthClient, + @MockkBean val categoryService: CategoryService, ) : MvcWebTestClientDescribeSpec( testMethodName = "RestApiServerControllerTest", webContext = webContext, From 237d318ba3e1f9cc0b3ac139308c8f2c7c256896 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 09:09:39 +0900 Subject: [PATCH 113/178] =?UTF-8?q?feat=20:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20theme,=20userId,=20category=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=8F=AC=ED=95=A8=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/core/server/dto/ServerThemeInfo.kt | 6 ++++++ .../server/dto/request/CreateServerRequest.kt | 3 +++ .../adapter/output/mongo/entity/ServerEntity.kt | 12 ++++++++++++ .../main/kotlin/kpring/server/domain/Server.kt | 2 ++ .../main/kotlin/kpring/server/domain/Theme.kt | 17 +++++++++++++++++ .../input/rest/RestApiServerControllerTest.kt | 9 +++++++-- .../port/output/SaveServerPortTest.kt | 6 +++++- .../port/output/UpdateServerPortTest.kt | 4 ++-- .../kotlin/kpring/server/domain/ServerTest.kt | 16 ++++++++++++++++ .../test/restdoc/dsl/RestDocBodyBuilder.kt | 10 ++++++++-- 10 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 core/src/main/kotlin/kpring/core/server/dto/ServerThemeInfo.kt create mode 100644 server/src/main/kotlin/kpring/server/domain/Theme.kt diff --git a/core/src/main/kotlin/kpring/core/server/dto/ServerThemeInfo.kt b/core/src/main/kotlin/kpring/core/server/dto/ServerThemeInfo.kt new file mode 100644 index 00000000..5e8809d2 --- /dev/null +++ b/core/src/main/kotlin/kpring/core/server/dto/ServerThemeInfo.kt @@ -0,0 +1,6 @@ +package kpring.core.server.dto + +data class ServerThemeInfo( + val id: String, + val name: String, +) diff --git a/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt b/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt index b35d8fe6..ed3a8cf1 100644 --- a/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt +++ b/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt @@ -2,4 +2,7 @@ package kpring.core.server.dto.request data class CreateServerRequest( val serverName: String, + val userId: String, + val theme: String? = null, + val categories: List = listOf(), ) diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt index 6c38322c..48a74bcf 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt @@ -1,14 +1,26 @@ package kpring.server.adapter.output.mongo.entity +import kpring.server.domain.Category import kpring.server.domain.Server +import kpring.server.domain.Theme import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document +/** + * @property id MongoDB의 ID를 나타냅니다. + * @property name 서버의 이름을 나타냅니다. + * @property users 서버에 속한 사용자의 아이디를 나타냅니다. 더 상세한 서버 유저에 대한 정보는 [ServerProfileEntity] 에서 찾을 수 있습니다. + * @property invitedUserIds 서버에 구성원은 아니지만 서버에 초대된 사용자의 아이디를 나타냅니다. 초대된 사용자는 서버에 가입할 수 있는 권한을 얻게 됩니다. + * @property theme 서버의 테마 정보를 나타냅니다. 테마 정보를 입력하지 않는다면 디폴트 값이 설정되며 디폴트 값은 [Theme.default] 입니다. + * @property categories 서버의 카테고리 정보를 나타냅니다. 카테고리 정보를 입력하지 않는다면 카테고리가 없는 서버를 생성합니다. + */ @Document(collection = "server") class ServerEntity( var name: String, var users: MutableList = mutableListOf(), var invitedUserIds: MutableList = mutableListOf(), + val theme: Theme = Theme.default(), + val categories: Set = emptySet(), ) { @Id lateinit var id: String diff --git a/server/src/main/kotlin/kpring/server/domain/Server.kt b/server/src/main/kotlin/kpring/server/domain/Server.kt index 0b61384b..85bc45c4 100644 --- a/server/src/main/kotlin/kpring/server/domain/Server.kt +++ b/server/src/main/kotlin/kpring/server/domain/Server.kt @@ -8,6 +8,8 @@ class Server( val name: String, val users: MutableSet = mutableSetOf(), val invitedUserIds: MutableSet = mutableSetOf(), + val theme: Theme = Theme.default(), + val categories: Set = emptySet(), ) { private fun isInvited(userId: String): Boolean { return invitedUserIds.contains(userId) diff --git a/server/src/main/kotlin/kpring/server/domain/Theme.kt b/server/src/main/kotlin/kpring/server/domain/Theme.kt new file mode 100644 index 00000000..55998fd1 --- /dev/null +++ b/server/src/main/kotlin/kpring/server/domain/Theme.kt @@ -0,0 +1,17 @@ +package kpring.server.domain + +enum class Theme( + val title: String, +) { + SERVER_THEME_001("숲"), + SERVER_THEME_002("오피스"), + ; + + companion object { + fun default() = SERVER_THEME_001 + } + + fun id(): String = this.name + + fun displayName(): String = this.title +} diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index 1c0898ee..ecb5da0d 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -59,7 +59,9 @@ class RestApiServerControllerTest( val url = "/api/v1/server" it("요청 성공시") { // given - val request = CreateServerRequest(serverName = "test server") + val userId = "test_user_id" + + val request = CreateServerRequest(serverName = "test server", userId = userId) val data = CreateServerResponse(serverId = "1", serverName = request.serverName) every { authClient.getTokenInfo(any()) } returns @@ -67,7 +69,7 @@ class RestApiServerControllerTest( data = TokenInfo( type = TokenType.ACCESS, - userId = "test_user_id", + userId = userId, ), ) every { serverService.createServer(eq(request), any()) } returns data @@ -96,6 +98,9 @@ class RestApiServerControllerTest( header { "Authorization" mean "jwt access token" } body { "serverName" type Strings mean "생성할 서버의 이름" + "userId" type Strings mean "서버를 생성하는 유저의 id" + "theme" type Strings mean "생성할 서버의 테마" optional true + "categories" type Arrays mean "생성할 서버의 카테고리 목록" optional true } } diff --git a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt index 828d5cc7..5511a71b 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt @@ -18,7 +18,11 @@ class SaveServerPortTest( it("서버를 저장하면 생성한 유저는 서버의 소유자가 된다.") { // given val userId = "userId" - val req = CreateServerRequest(serverName = "serverName") + val req = + CreateServerRequest( + serverName = "serverName", + userId = userId, + ) // when val server = saveServerPort.create(req, userId) diff --git a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt index a82dbb2a..261bec1d 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt @@ -19,7 +19,7 @@ class UpdateServerPortTest( it("유저를 초대가 작동한다.") { // given val userId = "userId" - val server = createServerPort.create(CreateServerRequest("serverName"), userId) + val server = createServerPort.create(CreateServerRequest("serverName", userId), userId) // when repeat(5) { @@ -34,7 +34,7 @@ class UpdateServerPortTest( it("가입 유저를 추가할 수 있다.") { // given val ownerId = "ownerId" - val server = createServerPort.create(CreateServerRequest("serverName"), ownerId) + val server = createServerPort.create(CreateServerRequest("serverName", ownerId), ownerId) val userId = "userId" server.registerInvitation(userId) diff --git a/server/src/test/kotlin/kpring/server/domain/ServerTest.kt b/server/src/test/kotlin/kpring/server/domain/ServerTest.kt index 43ce25ef..33373d3c 100644 --- a/server/src/test/kotlin/kpring/server/domain/ServerTest.kt +++ b/server/src/test/kotlin/kpring/server/domain/ServerTest.kt @@ -51,4 +51,20 @@ class ServerTest : DescribeSpec({ // then result shouldBe ServerErrorCode.ALREADY_REGISTERED_USER } + + it("테마를 지정하지 않은 서버는 기본 테마를 가진다.") { + // given & when + val server = Server("serverId", "serverName") + + // then + server.theme shouldBe Theme.default() + } + + it("카테고리를 지정하지 않은 서버는 빈 카테고리 목록을 가진다.") { + // given & when + val server = Server("serverId", "serverName") + + // then + server.categories shouldBe emptySet() + } }) diff --git a/test/src/main/kotlin/kpring/test/restdoc/dsl/RestDocBodyBuilder.kt b/test/src/main/kotlin/kpring/test/restdoc/dsl/RestDocBodyBuilder.kt index f2654ab1..70a46de7 100644 --- a/test/src/main/kotlin/kpring/test/restdoc/dsl/RestDocBodyBuilder.kt +++ b/test/src/main/kotlin/kpring/test/restdoc/dsl/RestDocBodyBuilder.kt @@ -7,9 +7,15 @@ import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath class RestDocBodyBuilder { val bodyFields = mutableListOf() - infix fun FieldDescriptor.mean(description: String) { - bodyFields.add(this.description(description)) + infix fun FieldDescriptor.mean(description: String): FieldDescriptor { + val descriptor = this.description(description) + bodyFields.add(descriptor) + return descriptor } infix fun String.type(type: JsonDataType): FieldDescriptor = fieldWithPath(this).type(type.value) + + infix fun FieldDescriptor.optional(isOptional: Boolean): FieldDescriptor { + return if (isOptional) this.optional() else this + } } From 236fc14702774f0b71be6b5e60d2bb39a1bb7567 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 09:19:46 +0900 Subject: [PATCH 114/178] =?UTF-8?q?feat=20:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20userId=20validate=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/rest/RestApiServerController.kt | 12 ++++- .../output/mongo/SaveServerPortMongoImpl.kt | 7 +-- .../application/port/output/SaveServerPort.kt | 5 +- .../application/service/ServerService.kt | 2 +- .../input/rest/RestApiServerControllerTest.kt | 53 +++++++++++++++++++ .../port/output/SaveServerPortTest.kt | 2 +- .../port/output/UpdateServerPortTest.kt | 4 +- 7 files changed, 71 insertions(+), 14 deletions(-) diff --git a/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt b/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt index 6174ea6d..9006a9ca 100644 --- a/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt +++ b/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt @@ -27,9 +27,19 @@ class RestApiServerController( fun createServer( @RequestHeader("Authorization") token: String, @RequestBody request: CreateServerRequest, - ): ResponseEntity> { + ): ResponseEntity> { + // get and validate user token val userInfo = authClient.getTokenInfo(token).data!! + + // validate userId + if (userInfo.userId != request.userId) { + return ResponseEntity.badRequest() + .body(ApiResponse(message = "유저 정보가 일치하지 않습니다")) + } + + // logic val data = createServerUseCase.createServer(request, userInfo.userId) + return ResponseEntity.ok() .body(ApiResponse(data = data)) } diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt index ab6c86be..38a632a0 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt @@ -20,10 +20,7 @@ class SaveServerPortMongoImpl( @Value("\${resource.default.profileImagePath}") private lateinit var defaultImagePath: String - override fun create( - req: CreateServerRequest, - userId: String, - ): Server { + override fun create(req: CreateServerRequest): Server { // create server val serverEntity = serverRepository.save(ServerEntity(name = req.serverName)) @@ -31,7 +28,7 @@ class SaveServerPortMongoImpl( // create owner server profile serverProfileRepository.save( ServerProfileEntity( - userId = userId, + userId = req.userId, // todo change name = "USER_${UUID.randomUUID()}", // todo change diff --git a/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt b/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt index c62e6e14..fcc91507 100644 --- a/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt +++ b/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt @@ -4,8 +4,5 @@ import kpring.core.server.dto.request.CreateServerRequest import kpring.server.domain.Server interface SaveServerPort { - fun create( - req: CreateServerRequest, - userId: String, - ): Server + fun create(req: CreateServerRequest): Server } diff --git a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt index 5977c9fe..6fbdf9fc 100644 --- a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt +++ b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt @@ -34,7 +34,7 @@ class ServerService( req: CreateServerRequest, userId: String, ): CreateServerResponse { - val server = createServerPort.create(req, userId) + val server = createServerPort.create(req) return CreateServerResponse( serverId = server.id, serverName = server.name, diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index ecb5da0d..273bc7ab 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -112,6 +112,59 @@ class RestApiServerControllerTest( } } } + + it("요청 실패시 : 요청한 유저와 서버 권한을 가진 유저가 일치하지 않는 경우") { + // given + val serverOwnerId = "server owner id" + + val request = CreateServerRequest(serverName = "test server", userId = serverOwnerId) + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, + userId = "request user id", + ), + ) + + // when + val result = + client.post() + .uri(url) + .header("Authorization", "Bearer mock_token") + .bodyValue(request) + .exchange() + + // then + val docs = + result + .expectStatus().isBadRequest + .expectBody() + .json(om.writeValueAsString(ApiResponse(message = "유저 정보가 일치하지 않습니다"))) + + // docs + docs.restDoc( + identifier = "create_server_200", + description = "서버 생성 api", + ) { + request { + header { "Authorization" mean "jwt access token" } + body { + "serverName" type Strings mean "생성할 서버의 이름" + "userId" type Strings mean "서버를 생성하는 유저의 id" + "theme" type Strings mean "생성할 서버의 테마" optional true + "categories" type Arrays mean "생성할 서버의 카테고리 목록" optional true + } + } + + response { + body { + "message" type Strings mean "실패 메시지" + } + } + } + } } describe("GET /api/v1/server/{serverId}: 서버 조회 api test") { diff --git a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt index 5511a71b..642a2f66 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt @@ -25,7 +25,7 @@ class SaveServerPortTest( ) // when - val server = saveServerPort.create(req, userId) + val server = saveServerPort.create(req) val profile = getServerProfilePort.get(server.id, userId) // then diff --git a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt index 261bec1d..ff0c0b58 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt @@ -19,7 +19,7 @@ class UpdateServerPortTest( it("유저를 초대가 작동한다.") { // given val userId = "userId" - val server = createServerPort.create(CreateServerRequest("serverName", userId), userId) + val server = createServerPort.create(CreateServerRequest("serverName", userId)) // when repeat(5) { @@ -34,7 +34,7 @@ class UpdateServerPortTest( it("가입 유저를 추가할 수 있다.") { // given val ownerId = "ownerId" - val server = createServerPort.create(CreateServerRequest("serverName", ownerId), ownerId) + val server = createServerPort.create(CreateServerRequest("serverName", ownerId)) val userId = "userId" server.registerInvitation(userId) From 671fbb33c241e3acd01f77bc97d32702967c7fb4 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 09:21:39 +0900 Subject: [PATCH 115/178] =?UTF-8?q?refactor=20:=20CreateServerUseCase=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit userId가 중복해서 전달되는 createServer 메서드를 개선하였습니다. --- .../server/adapter/input/rest/RestApiServerController.kt | 2 +- .../server/application/port/input/CreateServerUseCase.kt | 5 +---- .../kpring/server/application/service/ServerService.kt | 5 +---- .../server/adapter/input/rest/RestApiServerControllerTest.kt | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt b/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt index 9006a9ca..20a0e77a 100644 --- a/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt +++ b/server/src/main/kotlin/kpring/server/adapter/input/rest/RestApiServerController.kt @@ -38,7 +38,7 @@ class RestApiServerController( } // logic - val data = createServerUseCase.createServer(request, userInfo.userId) + val data = createServerUseCase.createServer(request) return ResponseEntity.ok() .body(ApiResponse(data = data)) diff --git a/server/src/main/kotlin/kpring/server/application/port/input/CreateServerUseCase.kt b/server/src/main/kotlin/kpring/server/application/port/input/CreateServerUseCase.kt index 92dcb330..d3c5a1ac 100644 --- a/server/src/main/kotlin/kpring/server/application/port/input/CreateServerUseCase.kt +++ b/server/src/main/kotlin/kpring/server/application/port/input/CreateServerUseCase.kt @@ -4,8 +4,5 @@ import kpring.core.server.dto.request.CreateServerRequest import kpring.core.server.dto.response.CreateServerResponse interface CreateServerUseCase { - fun createServer( - req: CreateServerRequest, - userId: String, - ): CreateServerResponse + fun createServer(req: CreateServerRequest): CreateServerResponse } diff --git a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt index 6fbdf9fc..07bc5881 100644 --- a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt +++ b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt @@ -30,10 +30,7 @@ class ServerService( val updateServerPort: UpdateServerPort, val deleteServerPort: DeleteServerPort, ) : CreateServerUseCase, GetServerInfoUseCase, AddUserAtServerUseCase, DeleteServerUseCase { - override fun createServer( - req: CreateServerRequest, - userId: String, - ): CreateServerResponse { + override fun createServer(req: CreateServerRequest): CreateServerResponse { val server = createServerPort.create(req) return CreateServerResponse( serverId = server.id, diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index 273bc7ab..509b369b 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -72,7 +72,7 @@ class RestApiServerControllerTest( userId = userId, ), ) - every { serverService.createServer(eq(request), any()) } returns data + every { serverService.createServer(eq(request)) } returns data // when val result = From 2cfe61b550cd58a06b70ce9cf8b8cc4ff0a649ef Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 10:35:13 +0900 Subject: [PATCH 116/178] =?UTF-8?q?refactor=20:=20entity=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/dto/request/CreateServerRequest.kt | 2 +- .../output/mongo/GetServerPortMongoImpl.kt | 17 +----- .../output/mongo/GetServerProfileMongoImpl.kt | 5 +- .../output/mongo/SaveServerPortMongoImpl.kt | 41 ++++++-------- .../output/mongo/entity/ServerEntity.kt | 24 +++++--- .../mongo/entity/ServerProfileEntity.kt | 16 +++++- .../application/port/output/SaveServerPort.kt | 3 +- .../application/service/ServerService.kt | 21 +++++-- .../kotlin/kpring/server/domain/Server.kt | 55 +++++++++++++++++-- .../kpring/server/domain/ServerProfile.kt | 1 + .../kpring/server/error/ServerErrorCode.kt | 2 + 11 files changed, 124 insertions(+), 63 deletions(-) diff --git a/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt b/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt index ed3a8cf1..262acc8e 100644 --- a/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt +++ b/core/src/main/kotlin/kpring/core/server/dto/request/CreateServerRequest.kt @@ -4,5 +4,5 @@ data class CreateServerRequest( val serverName: String, val userId: String, val theme: String? = null, - val categories: List = listOf(), + val categories: List? = null, ) diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerPortMongoImpl.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerPortMongoImpl.kt index 3a9d7911..a4b646b7 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerPortMongoImpl.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerPortMongoImpl.kt @@ -3,6 +3,7 @@ package kpring.server.adapter.output.mongo import kpring.core.global.exception.CommonErrorCode import kpring.core.global.exception.ServiceException import kpring.server.adapter.output.mongo.entity.QServerEntity +import kpring.server.adapter.output.mongo.entity.ServerEntity import kpring.server.adapter.output.mongo.repository.ServerRepository import kpring.server.application.port.output.GetServerPort import kpring.server.domain.Server @@ -17,12 +18,7 @@ class GetServerPortMongoImpl( serverRepository.findById(id) .orElseThrow { throw ServiceException(CommonErrorCode.NOT_FOUND) } - return Server( - id = serverEntity.id, - name = serverEntity.name, - users = serverEntity.users.toMutableSet(), - invitedUserIds = serverEntity.invitedUserIds.toMutableSet(), - ) + return serverEntity.toDomain() } override fun getServerWith(userId: String): List { @@ -32,13 +28,6 @@ class GetServerPortMongoImpl( server.users.any().eq(userId), ) - return servers.map { entity -> - Server( - id = entity.id, - name = entity.name, - users = entity.users.toMutableSet(), - invitedUserIds = entity.invitedUserIds.toMutableSet(), - ) - } + return servers.map(ServerEntity::toDomain) } } diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerProfileMongoImpl.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerProfileMongoImpl.kt index 68839b28..cf89c776 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerProfileMongoImpl.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/GetServerProfileMongoImpl.kt @@ -36,6 +36,7 @@ class GetServerProfileMongoImpl( val server = serverEntity.toDomain() val serverProfile = ServerProfile( + id = serverProfileEntity.id, server = server, userId = serverProfileEntity.userId, name = serverProfileEntity.name, @@ -60,7 +61,7 @@ class GetServerProfileMongoImpl( // get server val targetServerIds = serverProfileEntities - .map { it.serverId } + .map { it.serverId!! } val qServer = QServerEntity.serverEntity @@ -76,6 +77,7 @@ class GetServerProfileMongoImpl( val serverEntity = serverMap[it.serverId]!! val server = serverEntity.toDomain() ServerProfile( + id = serverEntity.id, server = server, userId = it.userId, name = it.name, @@ -97,6 +99,7 @@ class GetServerProfileMongoImpl( return serverProfileEntities.map { ServerProfile( + id = it.id, server = server, userId = it.userId, name = it.name, diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt index 38a632a0..09632178 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt @@ -1,16 +1,12 @@ package kpring.server.adapter.output.mongo -import kpring.core.server.dto.request.CreateServerRequest import kpring.server.adapter.output.mongo.entity.ServerEntity -import kpring.server.adapter.output.mongo.entity.ServerProfileEntity import kpring.server.adapter.output.mongo.repository.ServerProfileRepository import kpring.server.adapter.output.mongo.repository.ServerRepository import kpring.server.application.port.output.SaveServerPort import kpring.server.domain.Server -import kpring.server.domain.ServerRole import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository -import java.util.* @Repository class SaveServerPortMongoImpl( @@ -20,30 +16,25 @@ class SaveServerPortMongoImpl( @Value("\${resource.default.profileImagePath}") private lateinit var defaultImagePath: String - override fun create(req: CreateServerRequest): Server { + override fun create(server: Server): Server { // create server - val serverEntity = - serverRepository.save(ServerEntity(name = req.serverName)) + val serverEntity = serverRepository.save(ServerEntity(server)) - // create owner server profile - serverProfileRepository.save( - ServerProfileEntity( - userId = req.userId, - // todo change - name = "USER_${UUID.randomUUID()}", - // todo change - imagePath = defaultImagePath, - serverId = serverEntity.id, - role = ServerRole.OWNER, - bookmarked = false, - ), - ) +// // create owner server profile +// serverProfileRepository.save( +// ServerProfileEntity( +// userId = req.userId, +// // todo change +// name = "USER_${UUID.randomUUID()}", +// // todo change +// imagePath = defaultImagePath, +// serverId = serverEntity.id, +// role = ServerRole.OWNER, +// bookmarked = false, +// ), +// ) // mapping - return Server( - id = serverEntity.id, - name = serverEntity.name, - users = mutableSetOf(), - ) + return serverEntity.toDomain() } } diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt index 48a74bcf..dc3adf62 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerEntity.kt @@ -16,21 +16,31 @@ import org.springframework.data.mongodb.core.mapping.Document */ @Document(collection = "server") class ServerEntity( + @Id + val id: String?, var name: String, - var users: MutableList = mutableListOf(), - var invitedUserIds: MutableList = mutableListOf(), - val theme: Theme = Theme.default(), - val categories: Set = emptySet(), + var users: MutableSet, + var invitedUserIds: MutableSet, + val theme: Theme, + val categories: Set, ) { - @Id - lateinit var id: String + constructor(server: Server) : this( + id = server.id, + name = server.name, + users = server.users, + invitedUserIds = server.invitedUserIds, + theme = server.theme, + categories = server.categories, + ) fun toDomain(): Server { return Server( id = id, name = name, - users = users.toMutableSet(), + users = users, invitedUserIds = invitedUserIds.toMutableSet(), + theme = theme, + categories = categories, ) } } diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt index b9087519..edcf1b02 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/entity/ServerProfileEntity.kt @@ -1,18 +1,28 @@ package kpring.server.adapter.output.mongo.entity +import kpring.server.domain.ServerProfile import kpring.server.domain.ServerRole import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @Document("server_profile") class ServerProfileEntity( + @Id + val id: String?, val userId: String, val name: String, val imagePath: String, - val serverId: String, + val serverId: String?, val role: ServerRole, val bookmarked: Boolean, ) { - @Id - private var id: String? = null + constructor(profile: ServerProfile) : this( + id = profile.id, + userId = profile.userId, + name = profile.name, + imagePath = profile.imagePath, + serverId = profile.server.id, + role = profile.role, + bookmarked = profile.bookmarked, + ) } diff --git a/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt b/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt index fcc91507..f0cb75b6 100644 --- a/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt +++ b/server/src/main/kotlin/kpring/server/application/port/output/SaveServerPort.kt @@ -1,8 +1,7 @@ package kpring.server.application.port.output -import kpring.core.server.dto.request.CreateServerRequest import kpring.server.domain.Server interface SaveServerPort { - fun create(req: CreateServerRequest): Server + fun create(server: Server): Server } diff --git a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt index 07bc5881..2b0ca13f 100644 --- a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt +++ b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt @@ -18,6 +18,7 @@ import kpring.server.application.port.output.GetServerPort import kpring.server.application.port.output.GetServerProfilePort import kpring.server.application.port.output.SaveServerPort import kpring.server.application.port.output.UpdateServerPort +import kpring.server.domain.Server import kpring.server.domain.ServerAuthority import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -31,18 +32,26 @@ class ServerService( val deleteServerPort: DeleteServerPort, ) : CreateServerUseCase, GetServerInfoUseCase, AddUserAtServerUseCase, DeleteServerUseCase { override fun createServer(req: CreateServerRequest): CreateServerResponse { - val server = createServerPort.create(req) + val server = + createServerPort.create( + Server( + name = req.serverName, + users = mutableSetOf(req.userId), + theme = req.theme, + categories = req.categories, + ), + ) return CreateServerResponse( - serverId = server.id, + serverId = server.id!!, serverName = server.name, ) } override fun getServerInfo(serverId: String): ServerInfo { val server = getServer.get(serverId) - val serverProfiles = getServerProfilePort.getAll(server.id) + val serverProfiles = getServerProfilePort.getAll(server.id!!) return ServerInfo( - id = server.id, + id = server.id!!, name = server.name, users = serverProfiles.map { profile -> @@ -62,7 +71,7 @@ class ServerService( return getServerProfilePort.getProfiles(condition, userId) .map { profile -> ServerSimpleInfo( - id = profile.server.id, + id = profile.server.id!!, name = profile.server.name, bookmarked = profile.bookmarked, ) @@ -83,7 +92,7 @@ class ServerService( // register invitation val server = serverProfile.server server.registerInvitation(userId) - updateServerPort.inviteUser(server.id, userId) + updateServerPort.inviteUser(server.id!!, userId) } @Transactional diff --git a/server/src/main/kotlin/kpring/server/domain/Server.kt b/server/src/main/kotlin/kpring/server/domain/Server.kt index 85bc45c4..8ef863b9 100644 --- a/server/src/main/kotlin/kpring/server/domain/Server.kt +++ b/server/src/main/kotlin/kpring/server/domain/Server.kt @@ -4,13 +4,60 @@ import kpring.core.global.exception.ServiceException import kpring.server.error.ServerErrorCode class Server( - val id: String, + val id: String?, val name: String, val users: MutableSet = mutableSetOf(), val invitedUserIds: MutableSet = mutableSetOf(), - val theme: Theme = Theme.default(), - val categories: Set = emptySet(), + val theme: Theme, + val categories: Set, ) { + constructor( + name: String, + users: MutableSet = mutableSetOf(), + invitedUserIds: MutableSet = mutableSetOf(), + theme: String? = null, + categories: List? = null, + ) : this(null, name, users, invitedUserIds, initTheme(theme), initCategories(categories)) + + companion object { + // -----------start : 초기화 로직 ------------ + + /** + * 서버의 테마 정보를 초기화합니다. + * @param theme 서버의 테마 id를 나타냅니다. 테마 정보를 입력하지 않는다면 디폴트 값이 설정되며 디폴트 값은 [Theme.default] 입니다. + * @throws ServiceException 요청한 테마 id가 잘못된 경우 + */ + private fun initTheme(theme: String?): Theme { + if (theme == null) { + return Theme.default() + } + try { + return Theme.valueOf(theme) + } catch (e: IllegalArgumentException) { + throw ServiceException(ServerErrorCode.INVALID_THEME_ID) + } + } + + /** + * 서버의 카테고리 정보를 초기화합니다. + * @param categories 서버의 카테고리 id를 나타냅니다. 카테고리 정보를 입력하지 않는다면 카테고리가 없는 서버를 생성합니다. + * @throws ServiceException 요청한 카테고리 id가 잘못된 경우 + */ + private fun initCategories(categories: List?): Set { + if (categories == null) { + return setOf() + } + return categories.map { + try { + Category.valueOf(it) + } catch (e: IllegalArgumentException) { + throw ServiceException(ServerErrorCode.INVALID_CATEGORY_ID) + } + }.toSet() + } + // -----------end : 초기화 로직 -------------- + } + private fun isInvited(userId: String): Boolean { return invitedUserIds.contains(userId) } @@ -30,7 +77,7 @@ class Server( } else { throw ServiceException(ServerErrorCode.USER_NOT_INVITED) } - return ServerProfile(userId, name, imagePath, this) + return ServerProfile(null, userId, name, imagePath, this) } /** diff --git a/server/src/main/kotlin/kpring/server/domain/ServerProfile.kt b/server/src/main/kotlin/kpring/server/domain/ServerProfile.kt index f53673f5..e2ff599d 100644 --- a/server/src/main/kotlin/kpring/server/domain/ServerProfile.kt +++ b/server/src/main/kotlin/kpring/server/domain/ServerProfile.kt @@ -1,6 +1,7 @@ package kpring.server.domain class ServerProfile( + val id: String?, val userId: String, val name: String, val imagePath: String, diff --git a/server/src/main/kotlin/kpring/server/error/ServerErrorCode.kt b/server/src/main/kotlin/kpring/server/error/ServerErrorCode.kt index b3c0b6ce..4a7c148b 100644 --- a/server/src/main/kotlin/kpring/server/error/ServerErrorCode.kt +++ b/server/src/main/kotlin/kpring/server/error/ServerErrorCode.kt @@ -10,6 +10,8 @@ enum class ServerErrorCode( ) : ErrorCode { USER_NOT_INVITED("SERVER_001", "유저가 초대되지 않았습니다.", HttpStatus.FORBIDDEN), ALREADY_REGISTERED_USER("SERVER_002", "이미 등록된 유저입니다.", HttpStatus.BAD_REQUEST), + INVALID_THEME_ID("SERVER_003", "유효하지 않은 테마 아이디입니다.", HttpStatus.BAD_REQUEST), + INVALID_CATEGORY_ID("SERVER_004", "유효하지 않은 카테고리 아이디입니다.", HttpStatus.BAD_REQUEST), ; override fun id(): String = id From 36bd40f0f4ba9c1fc8a181fbe381a3a70a974678 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 10:59:35 +0900 Subject: [PATCH 117/178] =?UTF-8?q?test=20:=20entity=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../output/mongo/SaveServerPortMongoImpl.kt | 30 +++++++------ .../port/input/AddUserAtServerUseCaseTest.kt | 10 +++-- .../port/input/DeleteServerUseCaseTest.kt | 2 +- .../port/output/GetServerPortTest.kt | 16 +++---- .../port/output/GetServerProfilePortTest.kt | 44 +++++++++---------- .../port/output/SaveServerPortTest.kt | 12 ++--- .../port/output/UpdateServerPortTest.kt | 16 ++++--- .../kpring/server/domain/ServerProfileTest.kt | 2 +- .../kotlin/kpring/server/domain/ServerTest.kt | 6 +-- 9 files changed, 69 insertions(+), 69 deletions(-) diff --git a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt index 09632178..1b16d6d9 100644 --- a/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt +++ b/server/src/main/kotlin/kpring/server/adapter/output/mongo/SaveServerPortMongoImpl.kt @@ -1,12 +1,15 @@ package kpring.server.adapter.output.mongo import kpring.server.adapter.output.mongo.entity.ServerEntity +import kpring.server.adapter.output.mongo.entity.ServerProfileEntity import kpring.server.adapter.output.mongo.repository.ServerProfileRepository import kpring.server.adapter.output.mongo.repository.ServerRepository import kpring.server.application.port.output.SaveServerPort import kpring.server.domain.Server +import kpring.server.domain.ServerRole import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository +import java.util.* @Repository class SaveServerPortMongoImpl( @@ -20,19 +23,20 @@ class SaveServerPortMongoImpl( // create server val serverEntity = serverRepository.save(ServerEntity(server)) -// // create owner server profile -// serverProfileRepository.save( -// ServerProfileEntity( -// userId = req.userId, -// // todo change -// name = "USER_${UUID.randomUUID()}", -// // todo change -// imagePath = defaultImagePath, -// serverId = serverEntity.id, -// role = ServerRole.OWNER, -// bookmarked = false, -// ), -// ) + // create owner server profile + serverProfileRepository.save( + ServerProfileEntity( + id = null, + userId = server.users.first(), + // todo change + name = "USER_${UUID.randomUUID()}", + // todo change + imagePath = defaultImagePath, + serverId = serverEntity.id, + role = ServerRole.OWNER, + bookmarked = false, + ), + ) // mapping return serverEntity.toDomain() diff --git a/server/src/test/kotlin/kpring/server/application/port/input/AddUserAtServerUseCaseTest.kt b/server/src/test/kotlin/kpring/server/application/port/input/AddUserAtServerUseCaseTest.kt index 8766d4d7..ebe0f6e8 100644 --- a/server/src/test/kotlin/kpring/server/application/port/input/AddUserAtServerUseCaseTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/input/AddUserAtServerUseCaseTest.kt @@ -28,9 +28,11 @@ class AddUserAtServerUseCaseTest( // given val invitorId = "invitorId" val userId = "userId" - val server = Server(id = "serverId", name = "serverName") + val serverId = "serverId" + val server = Server(name = "serverName") val serverProfile = ServerProfile( + id = null, userId = invitorId, name = "invitor", imagePath = "imagePath", @@ -38,13 +40,13 @@ class AddUserAtServerUseCaseTest( server = server, ) - every { getServerPort.get(server.id) } returns server - every { getServerProfilePort.get(server.id, invitorId) } returns serverProfile + every { getServerPort.get(serverId) } returns server + every { getServerProfilePort.get(serverId, invitorId) } returns serverProfile // when val ex = shouldThrow { - service.inviteUser(server.id, invitorId, userId) + service.inviteUser(serverId, invitorId, userId) } // then diff --git a/server/src/test/kotlin/kpring/server/application/port/input/DeleteServerUseCaseTest.kt b/server/src/test/kotlin/kpring/server/application/port/input/DeleteServerUseCaseTest.kt index dd6f1722..0550431c 100644 --- a/server/src/test/kotlin/kpring/server/application/port/input/DeleteServerUseCaseTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/input/DeleteServerUseCaseTest.kt @@ -29,11 +29,11 @@ class DeleteServerUseCaseTest( val userId = "userId" val server = Server( - id = serverId, name = "serverName", ) val serverProfile = ServerProfile( + id = null, userId = userId, name = "name", imagePath = "/imagePath", diff --git a/server/src/test/kotlin/kpring/server/application/port/output/GetServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/GetServerPortTest.kt index 0fa27f89..6fedc724 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/GetServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/GetServerPortTest.kt @@ -9,6 +9,7 @@ import kpring.core.global.exception.CommonErrorCode import kpring.core.global.exception.ServiceException import kpring.server.adapter.output.mongo.entity.ServerEntity import kpring.server.adapter.output.mongo.repository.ServerRepository +import kpring.server.domain.Server import kpring.test.testcontainer.SpringTestContext import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration @@ -35,14 +36,12 @@ class GetServerPortTest( it("저장된 서버의 정보를 조회할 수 있다.") { // given - val userIds = mutableListOf("id") - val serverEntity = - serverRepository.save( - ServerEntity(name = "test", users = userIds), - ) + val userIds = mutableSetOf("id") + val domain = Server(name = "test", users = userIds) + val serverEntity = serverRepository.save(ServerEntity(domain)) // when - val server = getServerPort.get(serverEntity.id) + val server = getServerPort.get(serverEntity.id!!) // then server.name shouldBe "test" @@ -64,10 +63,11 @@ class GetServerPortTest( it("유저가 속한 서버 목록을 조회할 수 있다.") { // given val userId = "test-user" - val userIds = mutableListOf(userId) + val userIds = mutableSetOf(userId) + val server = Server(name = "server", users = userIds) repeat(2) { - serverRepository.save(ServerEntity(name = "test$it", users = userIds)) + serverRepository.save(ServerEntity(server)) } // when diff --git a/server/src/test/kotlin/kpring/server/application/port/output/GetServerProfilePortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/GetServerProfilePortTest.kt index fea97eff..8c7dedfb 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/GetServerProfilePortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/GetServerProfilePortTest.kt @@ -7,7 +7,10 @@ import kpring.server.adapter.output.mongo.entity.ServerEntity import kpring.server.adapter.output.mongo.entity.ServerProfileEntity import kpring.server.adapter.output.mongo.repository.ServerProfileRepository import kpring.server.adapter.output.mongo.repository.ServerRepository +import kpring.server.domain.Server +import kpring.server.domain.ServerProfile import kpring.server.domain.ServerRole +import kpring.server.domain.Theme import kpring.test.testcontainer.SpringTestContext import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration @@ -24,34 +27,29 @@ class GetServerProfilePortTest( it("제한된 서버 프로필을 조회하는 조건을 사용한다면 모든 프로필을 조회하지 않고 조건에 해당하는 서버 프로필만을 조회한다.") { // given - val userIds = mutableListOf("testUserId") - - val serverEntity1 = - serverRepository.save( - ServerEntity(name = "test", users = userIds), - ) - - val serverEntity2 = - serverRepository.save( - ServerEntity(name = "test", users = userIds), + val userIds = mutableSetOf("testUserId") + val server1 = Server(id = "testId", name = "test", users = userIds, theme = Theme.default(), categories = emptySet()) + val server2 = Server(name = "test", users = userIds) + + val serverEntity1 = serverRepository.save(ServerEntity(server1)) + val serverEntity2 = serverRepository.save(ServerEntity(server2)) + + val server1Profile = + ServerProfile( + id = null, + userId = "testUserId", + name = "test", + imagePath = "test", + role = ServerRole.MEMBER, + server = server1, ) - val serverProfileEntity = - serverProfileRepository.save( - ServerProfileEntity( - serverId = serverEntity1.id, - userId = "testUserId", - name = "test", - imagePath = "test", - role = ServerRole.MEMBER, - bookmarked = false, - ), - ) + val serverProfileEntity = serverProfileRepository.save(ServerProfileEntity(server1Profile)) - val condition = GetServerCondition(serverIds = listOf(serverEntity1.id)) + val condition = GetServerCondition(serverIds = listOf(serverEntity1.id!!)) // when - val serverProfiles = getServerProfilePort.getProfiles(condition, userIds[0]) + val serverProfiles = getServerProfilePort.getProfiles(condition, "testUserId") // then serverProfiles shouldHaveSize 1 diff --git a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt index 642a2f66..91b1c7ea 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/SaveServerPortTest.kt @@ -2,7 +2,7 @@ package kpring.server.application.port.output import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe -import kpring.core.server.dto.request.CreateServerRequest +import kpring.server.domain.Server import kpring.server.domain.ServerRole import kpring.test.testcontainer.SpringTestContext import org.springframework.boot.test.context.SpringBootTest @@ -18,15 +18,11 @@ class SaveServerPortTest( it("서버를 저장하면 생성한 유저는 서버의 소유자가 된다.") { // given val userId = "userId" - val req = - CreateServerRequest( - serverName = "serverName", - userId = userId, - ) + val domain = Server(name = "test", users = mutableSetOf(userId)) // when - val server = saveServerPort.create(req) - val profile = getServerProfilePort.get(server.id, userId) + val server = saveServerPort.create(domain) + val profile = getServerProfilePort.get(server.id!!, userId) // then profile.role shouldBe ServerRole.OWNER diff --git a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt index ff0c0b58..4a9c21c5 100644 --- a/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt +++ b/server/src/test/kotlin/kpring/server/application/port/output/UpdateServerPortTest.kt @@ -3,7 +3,7 @@ package kpring.server.application.port.output import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldHaveSize -import kpring.core.server.dto.request.CreateServerRequest +import kpring.server.domain.Server import kpring.test.testcontainer.SpringTestContext import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration @@ -19,33 +19,35 @@ class UpdateServerPortTest( it("유저를 초대가 작동한다.") { // given val userId = "userId" - val server = createServerPort.create(CreateServerRequest("serverName", userId)) + val domain = Server(name = "serverName", users = mutableSetOf(userId)) + val server = createServerPort.create(domain) // when repeat(5) { - updateServerPort.inviteUser(server.id, "test$it") + updateServerPort.inviteUser(server.id!!, "test$it") } // then - val result = getServerPort.get(server.id) + val result = getServerPort.get(server.id!!) result.invitedUserIds shouldHaveSize 5 } it("가입 유저를 추가할 수 있다.") { // given val ownerId = "ownerId" - val server = createServerPort.create(CreateServerRequest("serverName", ownerId)) + val domain = Server(name = "serverName", users = mutableSetOf(ownerId)) + val server = createServerPort.create(domain) val userId = "userId" server.registerInvitation(userId) - updateServerPort.inviteUser(server.id, userId) + updateServerPort.inviteUser(server.id!!, userId) val profile = server.addUser(userId, "name", "/path") // when updateServerPort.addUser(profile) // then - val result = getServerPort.get(server.id) + val result = getServerPort.get(server.id!!) result.users shouldContain userId result.invitedUserIds shouldHaveSize 0 } diff --git a/server/src/test/kotlin/kpring/server/domain/ServerProfileTest.kt b/server/src/test/kotlin/kpring/server/domain/ServerProfileTest.kt index 7e4ea526..e4609eac 100644 --- a/server/src/test/kotlin/kpring/server/domain/ServerProfileTest.kt +++ b/server/src/test/kotlin/kpring/server/domain/ServerProfileTest.kt @@ -11,13 +11,13 @@ class ServerProfileTest : DescribeSpec({ val userId = "invitedUserId" val server = Server( - id = "serverId", name = "serverName", invitedUserIds = mutableSetOf("invitedUserId"), users = mutableSetOf(userId), ) val serverProfile = ServerProfile( + id = null, server = server, name = "name", imagePath = "/imagePath", diff --git a/server/src/test/kotlin/kpring/server/domain/ServerTest.kt b/server/src/test/kotlin/kpring/server/domain/ServerTest.kt index 33373d3c..09533178 100644 --- a/server/src/test/kotlin/kpring/server/domain/ServerTest.kt +++ b/server/src/test/kotlin/kpring/server/domain/ServerTest.kt @@ -17,7 +17,6 @@ class ServerTest : DescribeSpec({ // given val server = Server( - "serverId", "serverName", mutableSetOf(), invitedUserIds = mutableSetOf("invitedUserId"), @@ -35,7 +34,6 @@ class ServerTest : DescribeSpec({ // given val server = Server( - "serverId", "serverName", mutableSetOf(), invitedUserIds = mutableSetOf("invitedUserId"), @@ -54,7 +52,7 @@ class ServerTest : DescribeSpec({ it("테마를 지정하지 않은 서버는 기본 테마를 가진다.") { // given & when - val server = Server("serverId", "serverName") + val server = Server("serverName") // then server.theme shouldBe Theme.default() @@ -62,7 +60,7 @@ class ServerTest : DescribeSpec({ it("카테고리를 지정하지 않은 서버는 빈 카테고리 목록을 가진다.") { // given & when - val server = Server("serverId", "serverName") + val server = Server("serverName") // then server.categories shouldBe emptySet() From 0fc454cdde02a1ee3e02545c4453fcdb70cd8937 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Wed, 12 Jun 2024 11:17:05 +0900 Subject: [PATCH 118/178] =?UTF-8?q?feat=20:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=91=EB=8B=B5=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EC=B6=94=EA=B0=80=20=ED=99=95=EC=9E=A5(theme,=20ca?= =?UTF-8?q?tegory)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/CreateServerResponse.kt | 5 +++++ .../application/service/ServerService.kt | 4 ++++ .../kotlin/kpring/server/util/DtoMapper.kt | 20 +++++++++++++++++++ .../input/rest/RestApiServerControllerTest.kt | 19 ++++++++++++++++-- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 server/src/main/kotlin/kpring/server/util/DtoMapper.kt diff --git a/core/src/main/kotlin/kpring/core/server/dto/response/CreateServerResponse.kt b/core/src/main/kotlin/kpring/core/server/dto/response/CreateServerResponse.kt index 888e930c..2edcf252 100644 --- a/core/src/main/kotlin/kpring/core/server/dto/response/CreateServerResponse.kt +++ b/core/src/main/kotlin/kpring/core/server/dto/response/CreateServerResponse.kt @@ -1,6 +1,11 @@ package kpring.core.server.dto.response +import kpring.core.server.dto.CategoryInfo +import kpring.core.server.dto.ServerThemeInfo + data class CreateServerResponse( val serverId: String, val serverName: String, + val theme: ServerThemeInfo, + val categories: List, ) diff --git a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt index 2b0ca13f..dc0189f0 100644 --- a/server/src/main/kotlin/kpring/server/application/service/ServerService.kt +++ b/server/src/main/kotlin/kpring/server/application/service/ServerService.kt @@ -18,8 +18,10 @@ import kpring.server.application.port.output.GetServerPort import kpring.server.application.port.output.GetServerProfilePort import kpring.server.application.port.output.SaveServerPort import kpring.server.application.port.output.UpdateServerPort +import kpring.server.domain.Category import kpring.server.domain.Server import kpring.server.domain.ServerAuthority +import kpring.server.util.toInfo import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -44,6 +46,8 @@ class ServerService( return CreateServerResponse( serverId = server.id!!, serverName = server.name, + theme = server.theme.toInfo(), + categories = server.categories.map(Category::toInfo), ) } diff --git a/server/src/main/kotlin/kpring/server/util/DtoMapper.kt b/server/src/main/kotlin/kpring/server/util/DtoMapper.kt new file mode 100644 index 00000000..244192cf --- /dev/null +++ b/server/src/main/kotlin/kpring/server/util/DtoMapper.kt @@ -0,0 +1,20 @@ +package kpring.server.util + +import kpring.core.server.dto.CategoryInfo +import kpring.core.server.dto.ServerThemeInfo +import kpring.server.domain.Category +import kpring.server.domain.Theme + +fun Category.toInfo(): CategoryInfo { + return CategoryInfo( + id = this.name, + name = this.toString(), + ) +} + +fun Theme.toInfo(): ServerThemeInfo { + return ServerThemeInfo( + id = this.id(), + name = this.displayName(), + ) +} diff --git a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt index 509b369b..9d16b16d 100644 --- a/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt +++ b/server/src/test/kotlin/kpring/server/adapter/input/rest/RestApiServerControllerTest.kt @@ -21,6 +21,9 @@ import kpring.core.server.dto.response.CreateServerResponse import kpring.server.application.service.CategoryService import kpring.server.application.service.ServerService import kpring.server.config.CoreConfiguration +import kpring.server.domain.Category +import kpring.server.domain.Theme +import kpring.server.util.toInfo import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType.* import kpring.test.web.MvcWebTestClientDescribeSpec @@ -62,7 +65,13 @@ class RestApiServerControllerTest( val userId = "test_user_id" val request = CreateServerRequest(serverName = "test server", userId = userId) - val data = CreateServerResponse(serverId = "1", serverName = request.serverName) + val data = + CreateServerResponse( + serverId = "1", + serverName = request.serverName, + theme = Theme.default().toInfo(), + categories = listOf(Category.SERVER_CATEGORY1, Category.SERVER_CATEGORY2).map(Category::toInfo), + ) every { authClient.getTokenInfo(any()) } returns ApiResponse( @@ -108,6 +117,12 @@ class RestApiServerControllerTest( body { "data.serverId" type Strings mean "서버 id" "data.serverName" type Strings mean "생성된 서버 이름" + + "data.theme.id" type Strings mean "테마 id" + "data.theme.name" type Strings mean "테마 이름" + + "data.categories[].id" type Strings mean "카테고리 id" + "data.categories[].name" type Strings mean "카테고리 이름" } } } @@ -145,7 +160,7 @@ class RestApiServerControllerTest( // docs docs.restDoc( - identifier = "create_server_200", + identifier = "create_server_fail-400", description = "서버 생성 api", ) { request { From b99176d848bf08719ae7717930bf75fa7d1f511b Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 17:31:21 +0900 Subject: [PATCH 119/178] =?UTF-8?q?refac:=20checkIfAccessTokenAndGetUserId?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20check,=20get=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/controller/FriendController.kt | 11 ++++++++--- .../kpring/user/controller/UserController.kt | 15 ++++++++++----- .../kotlin/kpring/user/global/AuthValidator.kt | 11 ++++------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/controller/FriendController.kt b/user/src/main/kotlin/kpring/user/controller/FriendController.kt index c41efb36..25600e73 100644 --- a/user/src/main/kotlin/kpring/user/controller/FriendController.kt +++ b/user/src/main/kotlin/kpring/user/controller/FriendController.kt @@ -1,5 +1,6 @@ package kpring.user.controller +import kpring.core.auth.client.AuthClient import kpring.core.global.dto.response.ApiResponse import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.* class FriendController( private val friendService: FriendService, private val authValidator: AuthValidator, + private val authClient: AuthClient, ) { @PostMapping("/user/{userId}/friend/{friendId}") fun addFriend( @@ -21,7 +23,8 @@ class FriendController( @PathVariable userId: Long, @PathVariable friendId: Long, ): ResponseEntity> { - val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val response = friendService.addFriend(userId, friendId) return ResponseEntity.ok(ApiResponse(data = response)) @@ -32,7 +35,8 @@ class FriendController( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { - authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + authValidator.checkIfAccessTokenAndGetUserId(validationResult) val response = friendService.getFriends(userId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -43,7 +47,8 @@ class FriendController( @PathVariable userId: Long, @PathVariable friendId: Long, ): ResponseEntity> { - val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val response = friendService.deleteFriend(userId, friendId) return ResponseEntity.ok(ApiResponse(data = response)) diff --git a/user/src/main/kotlin/kpring/user/controller/UserController.kt b/user/src/main/kotlin/kpring/user/controller/UserController.kt index a13a4aa2..0591cf18 100644 --- a/user/src/main/kotlin/kpring/user/controller/UserController.kt +++ b/user/src/main/kotlin/kpring/user/controller/UserController.kt @@ -1,5 +1,6 @@ package kpring.user.controller +import kpring.core.auth.client.AuthClient import kpring.core.global.dto.response.ApiResponse import kpring.user.dto.request.CreateUserRequest import kpring.user.dto.request.UpdateUserProfileRequest @@ -16,15 +17,17 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/v1") class UserController( - val userService: UserService, - val authValidator: AuthValidator, + private val userService: UserService, + private val authValidator: AuthValidator, + private val authClient: AuthClient, ) { @GetMapping("/user/{userId}") fun getUserProfile( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { - authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + authValidator.checkIfAccessTokenAndGetUserId(validationResult) val response = userService.getProfile(userId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -44,7 +47,8 @@ class UserController( @Validated @RequestPart(value = "json") request: UpdateUserProfileRequest, @RequestPart(value = "file") multipartFile: MultipartFile, ): ResponseEntity> { - val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val response = userService.updateProfile(userId, request, multipartFile) @@ -56,7 +60,8 @@ class UserController( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, ): ResponseEntity> { - val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(token) + val validationResult = authClient.getTokenInfo(token) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) val isExit = userService.exitUser(userId) diff --git a/user/src/main/kotlin/kpring/user/global/AuthValidator.kt b/user/src/main/kotlin/kpring/user/global/AuthValidator.kt index 33f652e3..51cc4c6c 100644 --- a/user/src/main/kotlin/kpring/user/global/AuthValidator.kt +++ b/user/src/main/kotlin/kpring/user/global/AuthValidator.kt @@ -1,21 +1,18 @@ package kpring.user.global -import kpring.core.auth.client.AuthClient +import kpring.core.auth.dto.response.TokenInfo import kpring.core.auth.enums.TokenType +import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.user.exception.UserErrorCode import org.springframework.stereotype.Component @Component -class AuthValidator( - private val authClient: AuthClient, -) { - fun checkIfAccessTokenAndGetUserId(token: String): String { - val validationResult = authClient.getTokenInfo(token) +class AuthValidator() { + fun checkIfAccessTokenAndGetUserId(validationResult: ApiResponse): String { if (validationResult.data!!.type != TokenType.ACCESS) { throw ServiceException(UserErrorCode.BAD_REQUEST) } - return validationResult.data!!.userId } From a88474e5dd0aa913577ff34d0309f57473906075 Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 17:32:01 +0900 Subject: [PATCH 120/178] =?UTF-8?q?chore:=20=EC=83=81=ED=83=9C=EB=B3=84=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/FriendRequestStatus.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt b/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt index 2c4667ef..a744b5d3 100644 --- a/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt +++ b/user/src/main/kotlin/kpring/user/entity/FriendRequestStatus.kt @@ -1,7 +1,7 @@ package kpring.user.entity enum class FriendRequestStatus { - REQUESTED, - RECEIVED, - ACCEPTED, + REQUESTED, // 친구신청을 보낸 상태 + RECEIVED, // 친구신청을 받은 상태 + ACCEPTED, // 친구신청을 수락한(친구가 된) 상태 } From e101402a0a0539c7054973a83114574fe645d3dd Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 17:35:14 +0900 Subject: [PATCH 121/178] =?UTF-8?q?refac:=20=EC=A0=91=EA=B7=BC=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EC=9E=90=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/entity/Friend.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/Friend.kt b/user/src/main/kotlin/kpring/user/entity/Friend.kt index 00d2754b..fd973685 100644 --- a/user/src/main/kotlin/kpring/user/entity/Friend.kt +++ b/user/src/main/kotlin/kpring/user/entity/Friend.kt @@ -7,13 +7,13 @@ import jakarta.persistence.* class Friend( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - var id: Long? = null, + private var id: Long? = null, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") - var user: User, + private var user: User, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "friend_id") - var friend: User, + private var friend: User, @Enumerated(EnumType.STRING) @Column(nullable = false) var requestStatus: FriendRequestStatus, From fae8c958d38e0dcd62f4393e2be61480aafebda1 Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 17:43:42 +0900 Subject: [PATCH 122/178] =?UTF-8?q?refac:=20=EC=B9=9C=EA=B5=AC=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EB=B0=8F=20=EC=8B=A0=EC=B2=AD=EB=B0=9B=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/User.kt | 22 ++++++++++++++----- .../kpring/user/service/FriendServiceImpl.kt | 5 ++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/User.kt b/user/src/main/kotlin/kpring/user/entity/User.kt index 2ed8275e..02f71211 100644 --- a/user/src/main/kotlin/kpring/user/entity/User.kt +++ b/user/src/main/kotlin/kpring/user/entity/User.kt @@ -20,11 +20,23 @@ class User( val friends: MutableSet = mutableSetOf(), // Other fields and methods... ) { - fun addFriendRelation( - friend: User, - requestStatus: FriendRequestStatus, - ) { - val friendRelation = Friend(user = this, friend = friend, requestStatus = requestStatus) + fun requestFriend(user: User) { + val friendRelation = + Friend( + user = this, + friend = user, + requestStatus = FriendRequestStatus.REQUESTED, + ) + friends.add(friendRelation) + } + + fun receiveFriendRequest(user: User) { + val friendRelation = + Friend( + user = this, + friend = user, + requestStatus = FriendRequestStatus.RECEIVED, + ) friends.add(friendRelation) } diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index f72099ea..3c90e16f 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -4,7 +4,6 @@ import kpring.core.global.exception.ServiceException import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse import kpring.user.dto.result.AddFriendResponse -import kpring.user.entity.FriendRequestStatus import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.repository.FriendRepository @@ -30,8 +29,8 @@ class FriendServiceImpl( checkSelfFriend(user, friend) checkFriendRelationExists(userId, friendId) - user.addFriendRelation(friend, FriendRequestStatus.REQUESTED) - friend.addFriendRelation(user, FriendRequestStatus.RECEIVED) + user.requestFriend(friend) + friend.receiveFriendRequest(user) return AddFriendResponse(friend.id!!) } From e3a461d0638394ce3e67dcc63f32875ebbc139f4 Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 18:07:09 +0900 Subject: [PATCH 123/178] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/FriendControllerTest.kt | 17 ++++++++++------- .../user/service/FriendServiceImplTest.kt | 8 ++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt index bbe50206..9377e36a 100644 --- a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt @@ -5,6 +5,9 @@ import com.ninjasquad.springmockk.MockkBean import io.kotest.core.spec.style.DescribeSpec import io.mockk.every import io.mockk.junit5.MockKExtension +import kpring.core.auth.client.AuthClient +import kpring.core.auth.dto.response.TokenInfo +import kpring.core.auth.enums.TokenType import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc @@ -30,6 +33,7 @@ internal class FriendControllerTest( webContext: WebApplicationContext, @MockkBean val friendService: FriendServiceImpl, @MockkBean val authValidator: AuthValidator, + @MockkBean val authClient: AuthClient, ) : DescribeSpec({ val restDocument = ManualRestDocumentation() @@ -53,6 +57,10 @@ internal class FriendControllerTest( // given val data = AddFriendResponse(friendId = CommonTest.TEST_FRIEND_ID) val response = ApiResponse(data = data) + + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns CommonTest.TEST_USER_ID.toString() @@ -103,10 +111,7 @@ internal class FriendControllerTest( // given val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() - every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws - ServiceException( - UserErrorCode.NOT_ALLOWED, - ) + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) // when val result = @@ -148,9 +153,7 @@ internal class FriendControllerTest( it("친구신청 실패 : 서버 내부 오류") { // given - every { - authValidator.checkIfAccessTokenAndGetUserId(any()) - } throws RuntimeException("서버 내부 오류") + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") val response = FailMessageResponse.serverError // when diff --git a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt index fb22aa37..b22d6861 100644 --- a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt @@ -5,7 +5,6 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.* import kpring.core.global.exception.ServiceException -import kpring.user.entity.FriendRequestStatus import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.global.CommonTest @@ -42,8 +41,8 @@ internal class FriendServiceImplTest : FunSpec({ every { friendRepository.existsByUserIdAndFriendId(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) } returns false - every { user.addFriendRelation(friend, FriendRequestStatus.REQUESTED) } just Runs - every { friend.addFriendRelation(user, FriendRequestStatus.RECEIVED) } just Runs + every { user.requestFriend(friend) } just Runs + every { friend.receiveFriendRequest(user) } just Runs val response = friendService.addFriend(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) response.friendId shouldBe friend.id @@ -71,7 +70,8 @@ internal class FriendServiceImplTest : FunSpec({ } exception.errorCode.message() shouldBe "자기자신에게 친구요청을 보낼 수 없습니다" - verify(exactly = 0) { user.addFriendRelation(any(), any()) } + verify(exactly = 0) { user.requestFriend(any()) } + verify(exactly = 0) { user.receiveFriendRequest(any()) } } test("친구신청_실패_이미 친구인 케이스") { From ccb819be87f7cd73f1267d914d45605e26bd8651 Mon Sep 17 00:00:00 2001 From: minahYu Date: Wed, 12 Jun 2024 18:26:31 +0900 Subject: [PATCH 124/178] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerTest.kt | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt index 5cd805dd..9b8f4fd5 100644 --- a/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/UserControllerTest.kt @@ -7,6 +7,8 @@ import io.mockk.clearMocks import io.mockk.every import io.mockk.junit5.MockKExtension import kpring.core.auth.client.AuthClient +import kpring.core.auth.dto.response.TokenInfo +import kpring.core.auth.enums.TokenType import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc @@ -19,6 +21,7 @@ import kpring.user.dto.response.GetUserProfileResponse import kpring.user.dto.response.UpdateUserProfileResponse import kpring.user.exception.UserErrorCode import kpring.user.global.AuthValidator +import kpring.user.global.CommonTest import kpring.user.service.UserService import org.junit.jupiter.api.extension.ExtendWith import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @@ -296,6 +299,9 @@ class UserControllerTest( val requestJson = objectMapper.writeValueAsString(request) val response = ApiResponse(data = data) + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit every { userService.updateProfile(userId, any(), any()) } returns data @@ -382,10 +388,7 @@ class UserControllerTest( val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() - every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws - ServiceException( - UserErrorCode.NOT_ALLOWED, - ) + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) val bodyBuilder = MultipartBodyBuilder() @@ -447,7 +450,6 @@ class UserControllerTest( it("회원정보 수정 실패 : 서버 내부 오류") { // given val userId = 1L - val token = "Bearer token" val request = UpdateUserProfileRequest.builder() .email(TEST_EMAIL) @@ -467,7 +469,7 @@ class UserControllerTest( val requestJson = objectMapper.writeValueAsString(request) val response = FailMessageResponse.serverError - every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws RuntimeException("서버 내부 오류") + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") val bodyBuilder = MultipartBodyBuilder() @@ -538,6 +540,9 @@ class UserControllerTest( .username(TEST_USERNAME) .build() val response = ApiResponse(data = data) + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() every { userService.getProfile(userId) } returns data @@ -584,10 +589,7 @@ class UserControllerTest( val token = "Bearer test" val response = FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() - every { authValidator.checkIfAccessTokenAndGetUserId(any()) } throws - ServiceException( - UserErrorCode.NOT_ALLOWED, - ) + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) // when val result = @@ -622,9 +624,7 @@ class UserControllerTest( // given val userId = 1L val token = "Bearer test" - every { - authValidator.checkIfAccessTokenAndGetUserId(any()) - } throws RuntimeException("서버 내부 오류") + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") val response = FailMessageResponse.serverError // when @@ -659,6 +659,9 @@ class UserControllerTest( it("탈퇴 성공") { // given val userId = 1L + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) every { authValidator.checkIfAccessTokenAndGetUserId(any()) } returns userId.toString() every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit every { userService.exitUser(userId) } returns true @@ -694,9 +697,7 @@ class UserControllerTest( it("탈퇴 실패 : 권한이 없는 토큰") { // given val userId = 1L - every { - authValidator.checkIfAccessTokenAndGetUserId(any()) - } throws ServiceException(UserErrorCode.NOT_ALLOWED) + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) // when val result = @@ -727,10 +728,7 @@ class UserControllerTest( it("탈퇴 실패 : 서버 내부 오류") { // given val userId = 1L - val token = "Bearer token" - every { - authValidator.checkIfAccessTokenAndGetUserId(any()) - } throws RuntimeException("서버 내부 오류") + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") // when val result = From f387b32f25d03aff13f301c93d04eccbe4a4a715 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 22:04:42 +0900 Subject: [PATCH 125/178] =?UTF-8?q?feat:=20verifyIfAuthor=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20private=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index aa0bbe2a..ccfcdebb 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -101,7 +101,7 @@ class ChatService( return true } - fun verifyIfAuthor( + private fun verifyIfAuthor( userId: String, chat: Chat, ) { From 63fee2d4fa33e21d5e9c96f369b9c9adc7c19531 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:54:26 +0900 Subject: [PATCH 126/178] =?UTF-8?q?feat:=20deleteChat=20Controller=20Layer?= =?UTF-8?q?=EC=97=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/api/v1/ChatController.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt index 7aa81a6d..78b9dee3 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt @@ -74,4 +74,20 @@ class ChatController( } return ResponseEntity.ok().body(ApiResponse(status = 200)) } + + @DeleteMapping("/chat/{chatId}") + fun deleteChat( + @RequestParam("type") type: String, + @PathVariable("chatId") chatId: String, + @RequestHeader("Authorization") token: String, + ): ResponseEntity<*> { + val userId = authClient.getTokenInfo(token).data!!.userId + val result = + when (type) { + ChatType.Room.toString() -> chatService.deleteRoomChat(chatId, userId) + ChatType.Server.toString() -> chatService.deleteServerChat(chatId, userId) + else -> throw GlobalException(ErrorCode.INVALID_CHAT_TYPE) + } + return ResponseEntity.ok().body(ApiResponse(status = 200)) + } } From ee3166e12bb6d7bafa470bd55c836a52a51fdf5d Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:55:50 +0900 Subject: [PATCH 127/178] =?UTF-8?q?feat:=20deleteRoomChat,=20deleteServerC?= =?UTF-8?q?hat=20Service=20Layer=EC=97=90=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/service/ChatService.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index ccfcdebb..518ff7bf 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -101,6 +101,26 @@ class ChatService( return true } + fun deleteRoomChat( + chatId: String, + userId: String, + ): Boolean { + val chat = roomChatRepository.findById(chatId).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } + verifyIfAuthor(userId, chat) + roomChatRepository.delete(chat) + return true + } + + fun deleteServerChat( + chatId: String, + userId: String, + ): Boolean { + val chat = serverChatRepository.findById(chatId).orElseThrow { GlobalException(ErrorCode.CHAT_NOT_FOUND) } + verifyIfAuthor(userId, chat) + serverChatRepository.delete(chat) + return true + } + private fun verifyIfAuthor( userId: String, chat: Chat, From 1a04e1cd5d1e49705a0de7cb1a5701bb917e3a52 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:56:20 +0900 Subject: [PATCH 128/178] =?UTF-8?q?feat:=20CreateChatRoomRequest=EC=9D=98?= =?UTF-8?q?=20var=EC=9D=84=20val=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/chat/chatroom/dto/request/CreateChatRoomRequest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/kpring/core/chat/chatroom/dto/request/CreateChatRoomRequest.kt b/core/src/main/kotlin/kpring/core/chat/chatroom/dto/request/CreateChatRoomRequest.kt index 0f583563..9582b872 100644 --- a/core/src/main/kotlin/kpring/core/chat/chatroom/dto/request/CreateChatRoomRequest.kt +++ b/core/src/main/kotlin/kpring/core/chat/chatroom/dto/request/CreateChatRoomRequest.kt @@ -4,5 +4,5 @@ import jakarta.validation.constraints.NotNull data class CreateChatRoomRequest( @field:NotNull - var users: List, + val users: List, ) From b6b2fe9fb262220c6ff4a0b2e1551f1b5350e210 Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:57:19 +0900 Subject: [PATCH 129/178] =?UTF-8?q?feat:=20ChatRoom=EC=97=90=20NoArg?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt index c309123f..58982c9b 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt @@ -1,9 +1,11 @@ package kpring.chat.chatroom.model +import kpring.chat.NoArg import kpring.chat.global.model.BaseTime import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document +@NoArg @Document(collection = "chatrooms") class ChatRoom : BaseTime() { @Id From 47c30bc4197d7977353aae84e4270adf291c0a7a Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:58:07 +0900 Subject: [PATCH 130/178] =?UTF-8?q?feat:=20ChatControllerTest=EC=97=90=20d?= =?UTF-8?q?eleteChat=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chat/api/v1/ChatControllerTest.kt | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt index 20fed416..dcc62d09 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt @@ -7,6 +7,7 @@ import io.mockk.every import io.mockk.junit5.MockKExtension import kpring.chat.chat.service.ChatService import kpring.chat.global.ChatRoomTest +import kpring.chat.global.ChatTest import kpring.chat.global.CommonTest import kpring.chat.global.config.TestMongoConfig import kpring.core.auth.client.AuthClient @@ -400,4 +401,131 @@ class ChatControllerTest( } } } + + describe("DELETE /api/v1/chat : deleteChat api test") { + + val url = "/api/v1/chat/{chatId}" + // Given + val userId = CommonTest.TEST_USER_ID + + it("deleteRoomChat api test") { + + // Given + val chatId = ChatTest.TEST_CHAT_ID + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = userId, + ), + ) + + every { + chatService.deleteRoomChat( + chatId, + userId, + ) + } returns true + + // When + val result = + webTestClient.delete().uri( + URLBuilder(url) + .query("type", "Room") + .build(), + chatId, + ) + .header("Authorization", "Bearer mock_token") + .exchange() + + val docs = + result + .expectStatus() + .isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(status = 200))) + + // Then + docs.restDoc( + identifier = "delete_room_chat_200", + description = "채팅방 채팅 삭제 api", + ) { + request { + query { + "type" mean "Server / Room" + } + path { + "chatId" mean "채팅 ID" + } + } + + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + + it("deleteServerChat api test") { + + // Given + val chatId = ChatTest.TEST_CHAT_ID + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = userId, + ), + ) + + every { + chatService.deleteServerChat( + chatId, + userId, + ) + } returns true + + // When + val result = + webTestClient.delete().uri( + URLBuilder(url) + .query("type", "Server") + .build(), + chatId, + ) + .header("Authorization", "Bearer mock_token") + .exchange() + + val docs = + result + .expectStatus() + .isOk + .expectBody() + .json(om.writeValueAsString(ApiResponse(status = 200))) + + // Then + docs.restDoc( + identifier = "delete_server_chat_200", + description = "서버 채팅 삭제 api", + ) { + request { + query { + "type" mean "Server / Room" + } + path { + "chatId" mean "채팅 ID" + } + } + + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + } }) From d56ef01a180123cbad980f8ad86554bc8f1e314b Mon Sep 17 00:00:00 2001 From: minisun Date: Wed, 12 Jun 2024 23:58:38 +0900 Subject: [PATCH 131/178] =?UTF-8?q?feat:=20ChatServiceTest=EC=97=90=20dele?= =?UTF-8?q?teRoomChat,deleteServerChat=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/ChatServiceTest.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index c75112c2..a6c7a873 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -192,4 +192,48 @@ class ChatServiceTest( result shouldBe true verify { roomChatRepository.save(any()) } } + + test("deleteServerChat 은 권한이 있는 사용자의 요청에 따라 Chat 삭제") { + // Given + val serverId = "test_server_id" + val chatId = "test_chat_id" + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + serverId, + "content", + ) + + every { serverChatRepository.findById(chatId) } returns Optional.of(chat) + every { serverChatRepository.delete(chat) } returns Unit + + // When + val result = chatService.deleteServerChat(chatId, userId) + + // Then + result shouldBe true + } + + test("deleteRoomChat 은 권한이 있는 사용자의 요청에 따라 Chat 삭제") { + // Given + val roomId = "test_room_id" + val chatId = "test_chat_id" + val userId = CommonTest.TEST_USER_ID + val chat = + Chat( + userId, + roomId, + "content", + ) + + every { roomChatRepository.findById(chatId) } returns Optional.of(chat) + every { roomChatRepository.delete(chat) } returns Unit + + // When + val result = chatService.deleteRoomChat(chatId, userId) + + // Then + result shouldBe true + } }) From 2b370df3b3eb6db362c8a877332d776f813cc454 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 15:52:12 +0900 Subject: [PATCH 132/178] =?UTF-8?q?refac:=20Service=20Layer=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20private=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt | 2 +- .../kotlin/kpring/chat/chatroom/service/ChatRoomService.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt index aa0bbe2a..ccfcdebb 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/service/ChatService.kt @@ -101,7 +101,7 @@ class ChatService( return true } - fun verifyIfAuthor( + private fun verifyIfAuthor( userId: String, chat: Chat, ) { diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index b528e3b5..f414687e 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -45,7 +45,7 @@ class ChatRoomService( return InvitationResponse(encodedCode) } - fun verifyChatRoomAccess( + private fun verifyChatRoomAccess( chatRoomId: String, userId: String, ) { @@ -54,7 +54,7 @@ class ChatRoomService( } } - fun getChatRoom(chatRoomId: String): ChatRoom { + private fun getChatRoom(chatRoomId: String): ChatRoom { val chatRoom: ChatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow { GlobalException(ErrorCode.CHATROOM_NOT_FOUND) } return chatRoom From 8347dafd95ec73f40a7b10dc3842230ab835ae18 Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 17:23:03 +0900 Subject: [PATCH 133/178] =?UTF-8?q?chore:=20=EC=9D=91=EB=8B=B5=20dto=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EB=AA=85=20response?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user/src/main/kotlin/kpring/user/controller/FriendController.kt | 2 +- .../kpring/user/dto/{result => response}/AddFriendResponse.kt | 2 +- user/src/main/kotlin/kpring/user/service/FriendService.kt | 2 +- user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt | 2 +- .../test/kotlin/kpring/user/controller/FriendControllerTest.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename user/src/main/kotlin/kpring/user/dto/{result => response}/AddFriendResponse.kt (62%) diff --git a/user/src/main/kotlin/kpring/user/controller/FriendController.kt b/user/src/main/kotlin/kpring/user/controller/FriendController.kt index 25600e73..52cc245c 100644 --- a/user/src/main/kotlin/kpring/user/controller/FriendController.kt +++ b/user/src/main/kotlin/kpring/user/controller/FriendController.kt @@ -2,9 +2,9 @@ package kpring.user.controller import kpring.core.auth.client.AuthClient import kpring.core.global.dto.response.ApiResponse +import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse -import kpring.user.dto.result.AddFriendResponse import kpring.user.global.AuthValidator import kpring.user.service.FriendService import org.springframework.http.ResponseEntity diff --git a/user/src/main/kotlin/kpring/user/dto/result/AddFriendResponse.kt b/user/src/main/kotlin/kpring/user/dto/response/AddFriendResponse.kt similarity index 62% rename from user/src/main/kotlin/kpring/user/dto/result/AddFriendResponse.kt rename to user/src/main/kotlin/kpring/user/dto/response/AddFriendResponse.kt index b479cbb5..73508dcf 100644 --- a/user/src/main/kotlin/kpring/user/dto/result/AddFriendResponse.kt +++ b/user/src/main/kotlin/kpring/user/dto/response/AddFriendResponse.kt @@ -1,4 +1,4 @@ -package kpring.user.dto.result +package kpring.user.dto.response data class AddFriendResponse( val friendId: Long, diff --git a/user/src/main/kotlin/kpring/user/service/FriendService.kt b/user/src/main/kotlin/kpring/user/service/FriendService.kt index 8dee2657..2751cd6f 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendService.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendService.kt @@ -1,8 +1,8 @@ package kpring.user.service +import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse -import kpring.user.dto.result.AddFriendResponse interface FriendService { fun getFriends(userId: Long): GetFriendsResponse diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index 3c90e16f..ac024944 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -1,9 +1,9 @@ package kpring.user.service import kpring.core.global.exception.ServiceException +import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse import kpring.user.dto.response.GetFriendsResponse -import kpring.user.dto.result.AddFriendResponse import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.repository.FriendRepository diff --git a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt index 9377e36a..7f58fdd7 100644 --- a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt @@ -12,8 +12,8 @@ import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc import kpring.test.restdoc.json.JsonDataType.Strings +import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.FailMessageResponse -import kpring.user.dto.result.AddFriendResponse import kpring.user.exception.UserErrorCode import kpring.user.global.AuthValidator import kpring.user.global.CommonTest From a2da747395756f2af310c5e46953331f5bc48965 Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 17:25:08 +0900 Subject: [PATCH 134/178] =?UTF-8?q?feat:=20GetFriendRequestResponse=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/dto/response/GetFriendRequestResponse.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestResponse.kt diff --git a/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestResponse.kt b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestResponse.kt new file mode 100644 index 00000000..2ca82ac2 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestResponse.kt @@ -0,0 +1,6 @@ +package kpring.user.dto.response + +data class GetFriendRequestResponse( + val friendId: Long, + val username: String, +) From 1bbcbd0b5b5ecc8f7e8beab1479541612b5d0ba9 Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 17:25:24 +0900 Subject: [PATCH 135/178] =?UTF-8?q?feat:=20GetFriendRequestsResponse=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/dto/response/GetFriendRequestsResponse.kt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt diff --git a/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt new file mode 100644 index 00000000..1a8329c4 --- /dev/null +++ b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt @@ -0,0 +1,6 @@ +package kpring.user.dto.response + +data class GetFriendRequestsResponse( + val userId: Long, + var friendRequests: MutableList, +) From db0117cb0cb50cf7e47251b37e0a781eefd6ee0b Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 17:33:56 +0900 Subject: [PATCH 136/178] =?UTF-8?q?feat:=20FriendController=EC=97=90=20?= =?UTF-8?q?=EC=B9=9C=EA=B5=AC=EC=8B=A0=EC=B2=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/FriendController.kt | 23 +++++++++++++++---- .../kpring/user/service/FriendService.kt | 3 +++ .../kpring/user/service/FriendServiceImpl.kt | 5 ++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/controller/FriendController.kt b/user/src/main/kotlin/kpring/user/controller/FriendController.kt index 52cc245c..094672a9 100644 --- a/user/src/main/kotlin/kpring/user/controller/FriendController.kt +++ b/user/src/main/kotlin/kpring/user/controller/FriendController.kt @@ -4,6 +4,7 @@ import kpring.core.auth.client.AuthClient import kpring.core.global.dto.response.ApiResponse import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse +import kpring.user.dto.response.GetFriendRequestsResponse import kpring.user.dto.response.GetFriendsResponse import kpring.user.global.AuthValidator import kpring.user.service.FriendService @@ -17,16 +18,15 @@ class FriendController( private val authValidator: AuthValidator, private val authClient: AuthClient, ) { - @PostMapping("/user/{userId}/friend/{friendId}") - fun addFriend( + @GetMapping("/user/{userId}/requests") + fun getFriendRequests( @RequestHeader("Authorization") token: String, @PathVariable userId: Long, - @PathVariable friendId: Long, - ): ResponseEntity> { + ): ResponseEntity> { val validationResult = authClient.getTokenInfo(token) val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) - val response = friendService.addFriend(userId, friendId) + val response = friendService.getFriendRequests(userId) return ResponseEntity.ok(ApiResponse(data = response)) } @@ -41,6 +41,19 @@ class FriendController( return ResponseEntity.ok(ApiResponse(data = response)) } + @PostMapping("/user/{userId}/friend/{friendId}") + fun addFriend( + @RequestHeader("Authorization") token: String, + @PathVariable userId: Long, + @PathVariable friendId: Long, + ): ResponseEntity> { + val validationResult = authClient.getTokenInfo(token) + val validatedUserId = authValidator.checkIfAccessTokenAndGetUserId(validationResult) + authValidator.checkIfUserIsSelf(userId.toString(), validatedUserId) + val response = friendService.addFriend(userId, friendId) + return ResponseEntity.ok(ApiResponse(data = response)) + } + @DeleteMapping("/user/{userId}/friend/{friendId}") fun deleteFriend( @RequestHeader("Authorization") token: String, diff --git a/user/src/main/kotlin/kpring/user/service/FriendService.kt b/user/src/main/kotlin/kpring/user/service/FriendService.kt index 2751cd6f..aa903558 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendService.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendService.kt @@ -2,9 +2,12 @@ package kpring.user.service import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse +import kpring.user.dto.response.GetFriendRequestsResponse import kpring.user.dto.response.GetFriendsResponse interface FriendService { + fun getFriendRequests(userId: Long): GetFriendRequestsResponse + fun getFriends(userId: Long): GetFriendsResponse fun addFriend( diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index ac024944..d60683fd 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -3,6 +3,7 @@ package kpring.user.service import kpring.core.global.exception.ServiceException import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.DeleteFriendResponse +import kpring.user.dto.response.GetFriendRequestsResponse import kpring.user.dto.response.GetFriendsResponse import kpring.user.entity.User import kpring.user.exception.UserErrorCode @@ -16,6 +17,10 @@ class FriendServiceImpl( private val userServiceImpl: UserServiceImpl, private val friendRepository: FriendRepository, ) : FriendService { + override fun getFriendRequests(userId: Long): GetFriendRequestsResponse { + TODO("Not yet implemented") + } + override fun getFriends(userId: Long): GetFriendsResponse { TODO("Not yet implemented") } From 8640c9db6f35834a0b587200f6e87c3f87e3bcd8 Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 17:59:59 +0900 Subject: [PATCH 137/178] =?UTF-8?q?feat:=20FriendRepository=EC=97=90=20fin?= =?UTF-8?q?dAllByUserIdAndRequestStatus=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/repository/FriendRepository.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt index ae0173b4..ee5bc406 100644 --- a/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt +++ b/user/src/main/kotlin/kpring/user/repository/FriendRepository.kt @@ -1,6 +1,7 @@ package kpring.user.repository import kpring.user.entity.Friend +import kpring.user.entity.FriendRequestStatus import org.springframework.data.jpa.repository.JpaRepository interface FriendRepository : JpaRepository { @@ -8,4 +9,9 @@ interface FriendRepository : JpaRepository { userId: Long, friendId: Long, ): Boolean + + fun findAllByUserIdAndRequestStatus( + userId: Long, + requestStatus: FriendRequestStatus, + ): List } From 08effd92a18bda63b53bc9e3dbaa4ad882d89522 Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 18:02:44 +0900 Subject: [PATCH 138/178] =?UTF-8?q?feat:=20FriendServiceImpl=EC=97=90=20ge?= =?UTF-8?q?tFriendRequests=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/entity/Friend.kt | 2 +- .../kpring/user/service/FriendServiceImpl.kt | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/entity/Friend.kt b/user/src/main/kotlin/kpring/user/entity/Friend.kt index fd973685..da9aeab5 100644 --- a/user/src/main/kotlin/kpring/user/entity/Friend.kt +++ b/user/src/main/kotlin/kpring/user/entity/Friend.kt @@ -13,7 +13,7 @@ class Friend( private var user: User, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "friend_id") - private var friend: User, + var friend: User, @Enumerated(EnumType.STRING) @Column(nullable = false) var requestStatus: FriendRequestStatus, diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index d60683fd..eb0a4672 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -1,10 +1,9 @@ package kpring.user.service import kpring.core.global.exception.ServiceException -import kpring.user.dto.response.AddFriendResponse -import kpring.user.dto.response.DeleteFriendResponse -import kpring.user.dto.response.GetFriendRequestsResponse -import kpring.user.dto.response.GetFriendsResponse +import kpring.user.dto.response.* +import kpring.user.entity.Friend +import kpring.user.entity.FriendRequestStatus import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.repository.FriendRepository @@ -18,7 +17,16 @@ class FriendServiceImpl( private val friendRepository: FriendRepository, ) : FriendService { override fun getFriendRequests(userId: Long): GetFriendRequestsResponse { - TODO("Not yet implemented") + val friendRelations: List = + friendRepository.findAllByUserIdAndRequestStatus(userId, FriendRequestStatus.RECEIVED) + val friendRequests: MutableList = mutableListOf() + + for (friendRelation in friendRelations) { + val friend = friendRelation.friend + friendRequests.add(GetFriendRequestResponse(friend.id!!, friend.username)) + } + + return GetFriendRequestsResponse(userId, friendRequests) } override fun getFriends(userId: Long): GetFriendsResponse { From 419bf3742133cb44c641a918a69da61c38a786ba Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 18:19:56 +0900 Subject: [PATCH 139/178] =?UTF-8?q?test:=20FriendControllerTest=EC=97=90?= =?UTF-8?q?=20=EC=B9=9C=EA=B5=AC=EC=8B=A0=EC=B2=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/FriendControllerTest.kt | 150 ++++++++++++++++++ .../kotlin/kpring/user/global/CommonTest.kt | 1 + 2 files changed, 151 insertions(+) diff --git a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt index 7f58fdd7..0367a6bf 100644 --- a/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt +++ b/user/src/test/kotlin/kpring/user/controller/FriendControllerTest.kt @@ -11,9 +11,12 @@ import kpring.core.auth.enums.TokenType import kpring.core.global.dto.response.ApiResponse import kpring.core.global.exception.ServiceException import kpring.test.restdoc.dsl.restDoc +import kpring.test.restdoc.json.JsonDataType import kpring.test.restdoc.json.JsonDataType.Strings import kpring.user.dto.response.AddFriendResponse import kpring.user.dto.response.FailMessageResponse +import kpring.user.dto.response.GetFriendRequestResponse +import kpring.user.dto.response.GetFriendRequestsResponse import kpring.user.exception.UserErrorCode import kpring.user.global.AuthValidator import kpring.user.global.CommonTest @@ -194,4 +197,151 @@ internal class FriendControllerTest( } } } + + describe("친구신청 조회 API") { + it("친구신청 조회 성공") { + // given + val friendRequest = + GetFriendRequestResponse( + friendId = CommonTest.TEST_FRIEND_ID, + username = CommonTest.TEST_FRIEND_USERNAME, + ) + val data = + GetFriendRequestsResponse( + userId = CommonTest.TEST_USER_ID, + friendRequests = mutableListOf(friendRequest), + ) + val response = ApiResponse(data = data) + + every { authClient.getTokenInfo(any()) }.returns( + ApiResponse(data = TokenInfo(TokenType.ACCESS, CommonTest.TEST_USER_ID.toString())), + ) + every { + authValidator.checkIfAccessTokenAndGetUserId(any()) + } returns CommonTest.TEST_USER_ID.toString() + every { authValidator.checkIfUserIsSelf(any(), any()) } returns Unit + every { + friendService.getFriendRequests(CommonTest.TEST_USER_ID) + } returns data + + // when + val result = + webTestClient.get() + .uri( + "/api/v1/user/{userId}/requests", + CommonTest.TEST_USER_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isOk + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "getFriendRequests200", + description = "친구신청 조회 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + "data.userId" type Strings mean "사용자 아이디" + "data.friendRequests" type JsonDataType.Arrays mean "친구신청한 사용자 리스트" + "data.friendRequests[].friendId" type Strings mean "친구신청한 사용자 아이디" + "data.friendRequests[].username" type Strings mean "친구신청한 사용자 닉네임" + } + } + } + } + it("친구신청 조회 실패 : 권한이 없는 토큰") { + // given + val response = + FailMessageResponse.builder().message(UserErrorCode.NOT_ALLOWED.message()).build() + every { authClient.getTokenInfo(any()) } throws ServiceException(UserErrorCode.NOT_ALLOWED) + + // when + val result = + webTestClient.get() + .uri( + "/api/v1/user/{userId}/requests", + CommonTest.TEST_USER_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isForbidden + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "getFriendRequests403", + description = "친구신청 조회 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + "message" type Strings mean "에러 메시지" + } + } + } + } + it("친구신청 조회 실패 : 서버 내부 오류") { + // given + val response = FailMessageResponse.serverError + every { authClient.getTokenInfo(any()) } throws RuntimeException("서버 내부 오류") + + // when + val result = + webTestClient.get() + .uri( + "/api/v1/user/{userId}/requests", + CommonTest.TEST_USER_ID, + ) + .header("Authorization", CommonTest.TEST_TOKEN) + .exchange() + + // then + val docsRoot = + result + .expectStatus().isEqualTo(500) + .expectBody().json(objectMapper.writeValueAsString(response)) + + // docs + docsRoot + .restDoc( + identifier = "getFriendRequests500", + description = "친구신청 조회 API", + ) { + request { + path { + "userId" mean "사용자 아이디" + } + header { "Authorization" mean "Bearer token" } + } + response { + body { + "message" type Strings mean "에러 메시지" + } + } + } + } + } }) diff --git a/user/src/test/kotlin/kpring/user/global/CommonTest.kt b/user/src/test/kotlin/kpring/user/global/CommonTest.kt index 80716bf7..9c550963 100644 --- a/user/src/test/kotlin/kpring/user/global/CommonTest.kt +++ b/user/src/test/kotlin/kpring/user/global/CommonTest.kt @@ -10,5 +10,6 @@ interface CommonTest { const val TEST_TOKEN = "Bearer test" const val TEST_FRIEND_ID = 2L + const val TEST_FRIEND_USERNAME = "friend" } } From 1763648bfbd5782c4546f666878f52cdfab02d6a Mon Sep 17 00:00:00 2001 From: minahYu Date: Fri, 14 Jun 2024 18:20:35 +0900 Subject: [PATCH 140/178] =?UTF-8?q?test:=20FriendServiceImplTest=EC=97=90?= =?UTF-8?q?=20=EC=B9=9C=EA=B5=AC=EC=8B=A0=EC=B2=AD=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/FriendServiceImplTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt index b22d6861..e5a24eaa 100644 --- a/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt +++ b/user/src/test/kotlin/kpring/user/service/FriendServiceImplTest.kt @@ -5,6 +5,8 @@ import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe import io.mockk.* import kpring.core.global.exception.ServiceException +import kpring.user.entity.Friend +import kpring.user.entity.FriendRequestStatus import kpring.user.entity.User import kpring.user.exception.UserErrorCode import kpring.user.global.CommonTest @@ -99,4 +101,19 @@ internal class FriendServiceImplTest : FunSpec({ friendService.checkFriendRelationExists(CommonTest.TEST_USER_ID, CommonTest.TEST_FRIEND_ID) } } + + test("친구신청조회_성공") { + val friend = mockk(relaxed = true) + val friendList = listOf(mockk(relaxed = true)) + + every { + friendRepository.findAllByUserIdAndRequestStatus(CommonTest.TEST_USER_ID, FriendRequestStatus.RECEIVED) + } returns friendList + + val response = friendService.getFriendRequests(CommonTest.TEST_USER_ID) + for (request in response.friendRequests) { + request.friendId shouldBe friend.id + request.username shouldBe friend.username + } + } }) From 7f3cf3dd178564de144fded260da8fcd664fc92d Mon Sep 17 00:00:00 2001 From: "BOOK-7HF2F58CR4\\sour1" Date: Sun, 9 Jun 2024 19:41:45 +0900 Subject: [PATCH 141/178] =?UTF-8?q?=EF=BB=BFfeat=20:=20webRtc=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=88=B4=EB=B0=94=20UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Map/ServerMap.tsx | 6 ++- .../components/VideoCall/VideoCallToolBar.tsx | 49 +++++++++++++++++++ front/tailwind.config.js | 8 ++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 front/src/components/VideoCall/VideoCallToolBar.tsx diff --git a/front/src/components/Map/ServerMap.tsx b/front/src/components/Map/ServerMap.tsx index 3a2c8c3f..f1e1fac2 100644 --- a/front/src/components/Map/ServerMap.tsx +++ b/front/src/components/Map/ServerMap.tsx @@ -4,7 +4,7 @@ import { ServerMapProps, ServerMapTypes } from "../../types/map"; import EnterServer from "./main"; import { MainMap } from "./ServerMap/MainMap"; import VideoCallBoxList from "../VideoCall/VideoCallBoxList"; - +import VideoCallToolBar from "../VideoCall/VideoCallToolBar"; // 서버를 생성하고 관리하는 컴포넌트 // forwardRef를 사용해 부모 컴포넌트로부터 ref를 전달 받음 @@ -127,6 +127,10 @@ export const ServerMap = forwardRef( +
+ +
+ ); } diff --git a/front/src/components/VideoCall/VideoCallToolBar.tsx b/front/src/components/VideoCall/VideoCallToolBar.tsx new file mode 100644 index 00000000..32fd45e2 --- /dev/null +++ b/front/src/components/VideoCall/VideoCallToolBar.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import MicIcon from '@mui/icons-material/Mic'; +import PhotoCameraFrontIcon from '@mui/icons-material/PhotoCameraFront'; +import MonitorIcon from '@mui/icons-material/Monitor'; +import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp'; + +// TODO : 비디오 툴바 +const VideoCallToolBar = () => { + const iconStyle = { + color: 'green', + padding: '2px', + boxSizing: 'content-box', + borderRadius: '3px', + '&:hover': { + backgroundColor: '#C7C8CC', + }, + }; + return ( +
+
+ + +
+
+ + +
+
+ +
+ +
+ ) +} + +export default VideoCallToolBar \ No newline at end of file diff --git a/front/tailwind.config.js b/front/tailwind.config.js index 79b30684..26abaafd 100644 --- a/front/tailwind.config.js +++ b/front/tailwind.config.js @@ -13,7 +13,13 @@ module.exports = { }, darkPink:{ DEFAULT : "#E5BEEC", - } + }, + gray:{ + DEFAULT : "hsla(0, 0%, 100%, .9)" + }, + darkGray:{ + DEFAULT : "rgb(39 38 46/var(--tw-text-opacity))" + }, }, }, }, From d9a26c4022d9fcbc94eecdedc7993c50a4ae78c3 Mon Sep 17 00:00:00 2001 From: yewon830 Date: Mon, 10 Jun 2024 21:04:45 +0900 Subject: [PATCH 142/178] =?UTF-8?q?feat=20:=20=EC=9B=B9=20RTC=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B3=B5=EC=9C=A0=20=EA=B8=B0=EB=8A=A5(=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EA=B5=AC=EB=B3=84x)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/VideoCall/VideoCallBoxList.tsx | 15 ++++++++---- .../VideoCall/VideoCallBoxListItem.tsx | 23 +++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/front/src/components/VideoCall/VideoCallBoxList.tsx b/front/src/components/VideoCall/VideoCallBoxList.tsx index 6ce26c57..45b339b2 100644 --- a/front/src/components/VideoCall/VideoCallBoxList.tsx +++ b/front/src/components/VideoCall/VideoCallBoxList.tsx @@ -8,10 +8,15 @@ import { Member } from '../../types/layout'; const VideoCallBoxList = () => { const [curVideoCallBoxPage, setCurVideoCallBoxPage] = useState(0); const [slicedMemberList, setSlicedMemberList] = useState([]); // 페이징 처리 된 멤버 리스트 - + const serverMemberList = [ { + id: 13, + userName: "test", + profilePath: "https://helpx.adobe.com/content/dam/help/en/photoshop/using/quick-actions/remove-background-before-qa1.png" + },]; + // 마지막 페이지 수 const lastPage = useMemo(()=>{ - const memberCnt = messageMemberList.length; + const memberCnt = serverMemberList.length; let lastPage = 0; if(memberCnt%4 === 0){ lastPage = Math.floor(memberCnt/4) - 1 @@ -40,7 +45,7 @@ const VideoCallBoxList = () => { // TODO : 화면공유 멤버 리스트 슬라이싱 함수 const sliceMemberList = useCallback(()=>{ - const newMemberList = messageMemberList.slice(curVideoCallBoxPage*4, (curVideoCallBoxPage*4)+4); + const newMemberList = serverMemberList.slice(curVideoCallBoxPage*4, (curVideoCallBoxPage*4)+4); setSlicedMemberList(newMemberList) },[curVideoCallBoxPage]) @@ -55,8 +60,8 @@ const VideoCallBoxList = () => { onClick={handleBoxPagePrev} />
{ - slicedMemberList.map(member=>( - + slicedMemberList.map((member,index)=>( + )) }
diff --git a/front/src/components/VideoCall/VideoCallBoxListItem.tsx b/front/src/components/VideoCall/VideoCallBoxListItem.tsx index 852cd279..dedbeec9 100644 --- a/front/src/components/VideoCall/VideoCallBoxListItem.tsx +++ b/front/src/components/VideoCall/VideoCallBoxListItem.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useRef } from 'react' import { Member } from '../../types/layout' import { Avatar } from '@mui/material'; @@ -7,10 +7,29 @@ interface MemberListItemProps{ } const VideoCallBoxListItem : React.FC= ({member}) => { + const videoRef = useRef(null); + const videoShare = async() => { + try{ + const stream = await navigator.mediaDevices.getDisplayMedia({ + video: true + }) + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + return stream + }catch(err){ + console.error("미디어 접근에 에러가 있습니다", err) + return null + } + } + useEffect(()=>{ + videoShare(); + },[]) + return (
- +
{member.userName}
From b7e5f2959d7ad01949a9b5580049449f506bf168 Mon Sep 17 00:00:00 2001 From: yewon830 Date: Fri, 14 Jun 2024 19:40:45 +0900 Subject: [PATCH 143/178] =?UTF-8?q?style=20:=20tailwind=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9->=20MUI=20sx=20=EC=86=8D=EC=84=B1=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/Chat/ChatRoomSideBar.tsx | 4 +- front/src/components/Layout/DropDown.tsx | 22 ++++--- .../components/Layout/FriendsRightSideBar.tsx | 32 ++++++---- front/src/components/Layout/MemberList.tsx | 13 +++- .../src/components/Layout/MemberListItem.tsx | 3 +- .../components/Layout/MessageRightSideBar.tsx | 32 ++++++---- .../components/VideoCall/VideoCallBoxList.tsx | 25 +++++--- .../VideoCall/VideoCallBoxListItem.tsx | 61 ++++++++++--------- .../components/VideoCall/VideoCallToolBar.tsx | 26 +++++--- 9 files changed, 136 insertions(+), 82 deletions(-) diff --git a/front/src/components/Chat/ChatRoomSideBar.tsx b/front/src/components/Chat/ChatRoomSideBar.tsx index 20a558b5..e247a3df 100644 --- a/front/src/components/Chat/ChatRoomSideBar.tsx +++ b/front/src/components/Chat/ChatRoomSideBar.tsx @@ -9,7 +9,6 @@ const ChatRoomSideBar = () => { setIsChatRoomShow(false); },[setIsChatRoomShow]) - const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", height: "8rem", @@ -23,8 +22,7 @@ const ChatRoomSideBar = () => {
다이렉트 메세지
diff --git a/front/src/components/Layout/DropDown.tsx b/front/src/components/Layout/DropDown.tsx index d442a0b9..374c26e8 100644 --- a/front/src/components/Layout/DropDown.tsx +++ b/front/src/components/Layout/DropDown.tsx @@ -1,4 +1,4 @@ -import { Box } from '@mui/material' +import { Box, List, ListItem } from '@mui/material' import React, { useMemo } from 'react' interface DropDownProps{ @@ -9,18 +9,24 @@ const DropDown: React.FC = ({dropDownItems}) => { const dropDownList = useMemo(()=>{ return dropDownItems.map((item, index)=>{ - return
  • {item}
  • + return {item} }) },[dropDownItems]) return ( - -
      - { - dropDownList - } -
    + + {dropDownList} ) } diff --git a/front/src/components/Layout/FriendsRightSideBar.tsx b/front/src/components/Layout/FriendsRightSideBar.tsx index 389de741..28b6c43b 100644 --- a/front/src/components/Layout/FriendsRightSideBar.tsx +++ b/front/src/components/Layout/FriendsRightSideBar.tsx @@ -16,7 +16,7 @@ import { messageMemberList } from "../../utils/fakeData"; const FriendsRightSideBar: React.FC = ({ close }) => { const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", - height: "8rem", + height: "128", alignItems: "center", padding: theme.spacing(0, 1), ...theme.mixins.toolbar, @@ -29,19 +29,29 @@ const FriendsRightSideBar: React.FC = ({ close }) => { return ( <> - - - - -
    친구 목록
    - + + + + + 친구 목록 + { openDropDown? : - + } { openDropDown && @@ -59,7 +69,7 @@ const FriendsRightSideBar: React.FC = ({ close }) => { }, } }} - sx={{paddingBottom: "1rem"}} + sx={{paddingBottom: "16px"}} /> diff --git a/front/src/components/Layout/MemberList.tsx b/front/src/components/Layout/MemberList.tsx index 19e86539..9b96ae7a 100644 --- a/front/src/components/Layout/MemberList.tsx +++ b/front/src/components/Layout/MemberList.tsx @@ -16,9 +16,16 @@ const MemberList: React.FC = ({ memberList }) => { return ( <> - - {memberList.map((member) => ( - + + {memberList.map((member,index) => ( + ))} diff --git a/front/src/components/Layout/MemberListItem.tsx b/front/src/components/Layout/MemberListItem.tsx index a1d74da7..e8797893 100644 --- a/front/src/components/Layout/MemberListItem.tsx +++ b/front/src/components/Layout/MemberListItem.tsx @@ -22,7 +22,8 @@ const MemberListItem : React.FC= ({member, handleProfileOpe return (
    - diff --git a/front/src/components/Layout/MessageRightSideBar.tsx b/front/src/components/Layout/MessageRightSideBar.tsx index 5cdd8ea1..78253038 100644 --- a/front/src/components/Layout/MessageRightSideBar.tsx +++ b/front/src/components/Layout/MessageRightSideBar.tsx @@ -17,7 +17,7 @@ import { messageMemberList } from "../../utils/fakeData"; const MessageRightSideBar: React.FC = ({ close }) => { const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", - height: "8rem", + height: "128px", alignItems: "center", padding: theme.spacing(0, 1), ...theme.mixins.toolbar, @@ -30,19 +30,29 @@ const MessageRightSideBar: React.FC = ({ close }) => { return ( <> - - - - -
    메세지 목록
    - + + + + + 메세지 목록 + { openDropDown? : - + } { openDropDown && @@ -60,7 +70,7 @@ const MessageRightSideBar: React.FC = ({ close }) => { }, } }} - sx={{paddingBottom: "1rem"}} + sx={{paddingBottom: "16px"}} /> diff --git a/front/src/components/VideoCall/VideoCallBoxList.tsx b/front/src/components/VideoCall/VideoCallBoxList.tsx index 45b339b2..0d9eeb69 100644 --- a/front/src/components/VideoCall/VideoCallBoxList.tsx +++ b/front/src/components/VideoCall/VideoCallBoxList.tsx @@ -1,9 +1,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import VideoCallBoxListItem from './VideoCallBoxListItem' -import { messageMemberList } from "../../utils/fakeData"; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import { Member } from '../../types/layout'; +import { Box } from '@mui/material'; const VideoCallBoxList = () => { const [curVideoCallBoxPage, setCurVideoCallBoxPage] = useState(0); @@ -54,22 +54,33 @@ const VideoCallBoxList = () => { }, [sliceMemberList]); return ( -
    + -
    + { slicedMemberList.map((member,index)=>( )) } -
    +
    -
    +
    ) } diff --git a/front/src/components/VideoCall/VideoCallBoxListItem.tsx b/front/src/components/VideoCall/VideoCallBoxListItem.tsx index dedbeec9..b61cc182 100644 --- a/front/src/components/VideoCall/VideoCallBoxListItem.tsx +++ b/front/src/components/VideoCall/VideoCallBoxListItem.tsx @@ -1,43 +1,44 @@ import React, { useEffect, useRef } from 'react' import { Member } from '../../types/layout' -import { Avatar } from '@mui/material'; +import { Box } from '@mui/material'; interface MemberListItemProps{ member: Member; } const VideoCallBoxListItem : React.FC= ({member}) => { - const videoRef = useRef(null); - const videoShare = async() => { - try{ - const stream = await navigator.mediaDevices.getDisplayMedia({ - video: true - }) - if (videoRef.current) { - videoRef.current.srcObject = stream; - } - return stream - }catch(err){ - console.error("미디어 접근에 에러가 있습니다", err) - return null - } - } - useEffect(()=>{ - videoShare(); - },[]) return ( -
    -
    - -
    -
    {member.userName}
    -
    - -
    - - -
    + + + {/* */} + + {member.userName} + + + ) } diff --git a/front/src/components/VideoCall/VideoCallToolBar.tsx b/front/src/components/VideoCall/VideoCallToolBar.tsx index 32fd45e2..d6033deb 100644 --- a/front/src/components/VideoCall/VideoCallToolBar.tsx +++ b/front/src/components/VideoCall/VideoCallToolBar.tsx @@ -3,6 +3,7 @@ import MicIcon from '@mui/icons-material/Mic'; import PhotoCameraFrontIcon from '@mui/icons-material/PhotoCameraFront'; import MonitorIcon from '@mui/icons-material/Monitor'; import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp'; +import { Box } from '@mui/material'; // TODO : 비디오 툴바 const VideoCallToolBar = () => { @@ -16,8 +17,17 @@ const VideoCallToolBar = () => { }, }; return ( -
    -
    + + { backgroundColor: '#C7C8CC', } }}/> -
    -
    + + { } }} /> -
    -
    + + -
    + -
    +
    ) } From 63bb5ecf835e1302ded85eb21c7591777d88cb82 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:07:37 +0900 Subject: [PATCH 144/178] =?UTF-8?q?refac:=20members=20List=EC=97=90?= =?UTF-8?q?=EC=84=9C=20Set=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt index c309123f..4db60d1d 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt @@ -9,9 +9,9 @@ class ChatRoom : BaseTime() { @Id var id: String? = null - var members: MutableList = mutableListOf() + var members: MutableSet = mutableSetOf() - fun getUsers(): List { + fun getUsers(): Set { return members } From 3c2bdbcae7fdef974e00614adae53d81f920d723 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:09:18 +0900 Subject: [PATCH 145/178] =?UTF-8?q?feat:=20ChatRoom=EC=97=90=20addUser=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt index 4db60d1d..4c9e5782 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt @@ -19,6 +19,10 @@ class ChatRoom : BaseTime() { members.addAll(list) } + fun addUser(userId: String) { + members.add(userId) + } + fun removeUser(userId: String) { members.remove(userId) } From b639231c856dc4d4199d1dc9432e4971a5edaa8f Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:09:54 +0900 Subject: [PATCH 146/178] =?UTF-8?q?feat:=20ChatRoomController=EC=97=90=20j?= =?UTF-8?q?oinChatRoom=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/api/v1/ChatRoomController.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt index 386e5702..fc061c8f 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/api/v1/ChatRoomController.kt @@ -45,4 +45,14 @@ class ChatRoomController( val result = chatRoomService.getChatRoomInvitation(chatRoomId, userId) return ResponseEntity.ok().body(ApiResponse(data = result)) } + + @PatchMapping("/chatroom/{code}/join") + fun joinChatRoom( + @PathVariable("code") code: String, + @RequestHeader("Authorization") token: String, + ): ResponseEntity<*> { + val userId = authClient.getTokenInfo(token).data!!.userId + val result = chatRoomService.joinChatRoom(code, userId) + return ResponseEntity.ok().body(ApiResponse(status = 200)) + } } From d026d16f23f31de1f58816d3f24525a51565e426 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:10:41 +0900 Subject: [PATCH 147/178] =?UTF-8?q?feat:=20ChatRoomService=EC=97=90=20veri?= =?UTF-8?q?fyInvitationExistence=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/service/ChatRoomService.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index f414687e..15f3c17c 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -1,5 +1,6 @@ package kpring.chat.chatroom.service +import kpring.chat.chatroom.dto.InvitationInfo import kpring.chat.chatroom.model.ChatRoom import kpring.chat.chatroom.repository.ChatRoomRepository import kpring.chat.global.exception.ErrorCode @@ -45,6 +46,13 @@ class ChatRoomService( return InvitationResponse(encodedCode) } + private fun verifyInvitationExistence(invitationInfo: InvitationInfo) { + if (invitationInfo.code != invitationService.getInvitation(invitationInfo.userId, invitationInfo.chatRoomId)) + { + throw GlobalException(ErrorCode.EXPIRED_INVITATION) + } + } + private fun verifyChatRoomAccess( chatRoomId: String, userId: String, From c8c3744006ab60bc45ec82f5e8f9a4e636072940 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:12:06 +0900 Subject: [PATCH 148/178] =?UTF-8?q?feat:=20ChatRoomService=EC=97=90=20join?= =?UTF-8?q?ChatRoom=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/chatroom/service/ChatRoomService.kt | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index 15f3c17c..bd923a49 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -46,11 +46,23 @@ class ChatRoomService( return InvitationResponse(encodedCode) } + fun joinChatRoom( + code: String, + userId: String, + ): Boolean { + val decodedCode = invitationService.decodeCode(code) + val invitationInfo = invitationService.getInvitationInfoFromCode(decodedCode) + verifyInvitationExistence(invitationInfo) + val chatRoom = getChatRoom(invitationInfo.chatRoomId) + chatRoom.addUser(userId) + chatRoomRepository.save(chatRoom) + return true + } + private fun verifyInvitationExistence(invitationInfo: InvitationInfo) { - if (invitationInfo.code != invitationService.getInvitation(invitationInfo.userId, invitationInfo.chatRoomId)) - { - throw GlobalException(ErrorCode.EXPIRED_INVITATION) - } + if (invitationInfo.code != invitationService.getInvitation(invitationInfo.userId, invitationInfo.chatRoomId)) { + throw GlobalException(ErrorCode.EXPIRED_INVITATION) + } } private fun verifyChatRoomAccess( From 580d5f16d373a9c2af846dc3569761cda13cca1c Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:12:24 +0900 Subject: [PATCH 149/178] =?UTF-8?q?feat:=20EXPIRED=5FINVITATION=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt index 7f1f3ec7..258e88a8 100644 --- a/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt +++ b/chat/src/main/kotlin/kpring/chat/global/exception/ErrorCode.kt @@ -15,6 +15,9 @@ enum class ErrorCode(val httpStatus: Int, val message: String) { CHATROOM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 chatroom을 찾을 수 없습니다"), CHAT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 id로 채팅을 찾을 수 없습니다"), + // 410 + EXPIRED_INVITATION(HttpStatus.GONE.value(), "만료된 Invitation입니다."), + // 500 INVITATION_LINK_SAVE_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Invitation Code가 저장되지 않았습니다"), } From 6fc3b9e054594f69c4c71888aea6555c446db581 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:12:40 +0900 Subject: [PATCH 150/178] =?UTF-8?q?feat:=20InvitationInfo=20DTO=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/chat/chatroom/dto/InvitationInfo.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 chat/src/main/kotlin/kpring/chat/chatroom/dto/InvitationInfo.kt diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/dto/InvitationInfo.kt b/chat/src/main/kotlin/kpring/chat/chatroom/dto/InvitationInfo.kt new file mode 100644 index 00000000..3e2230cf --- /dev/null +++ b/chat/src/main/kotlin/kpring/chat/chatroom/dto/InvitationInfo.kt @@ -0,0 +1,7 @@ +package kpring.chat.chatroom.dto + +data class InvitationInfo( + val userId: String, + val chatRoomId: String, + val code: String, +) From 01ebfe929c357636c5e9ce9fbe5ca0ebcc8f5487 Mon Sep 17 00:00:00 2001 From: minisun Date: Fri, 14 Jun 2024 23:13:29 +0900 Subject: [PATCH 151/178] =?UTF-8?q?feat:=20InvitationService=EC=97=90=20de?= =?UTF-8?q?codeCode,=20getInvitationInfoFromCode=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/service/InvitationService.kt | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt index 1748b1ca..a4ecb187 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt @@ -1,10 +1,10 @@ package kpring.chat.chatroom.service +import kpring.chat.chatroom.dto.InvitationInfo import kpring.chat.chatroom.repository.InvitationRepository import kpring.chat.global.config.ChatRoomProperty import org.springframework.stereotype.Service import java.nio.charset.StandardCharsets -import java.security.MessageDigest import java.util.* @Service @@ -36,17 +36,28 @@ class InvitationService( code: String, ): String { val key = generateKey(userId, chatRoomId) - return generateCode(key, code) + return encodeCode(key, code) } - private fun generateCode( + fun decodeCode(encodedString: String): String { + val decodedBytes = Base64.getUrlDecoder().decode(encodedString) + val decodedString = String(decodedBytes, StandardCharsets.UTF_8) + return decodedString + } + + fun getInvitationInfoFromCode(code: String): InvitationInfo { + val keyAndValue: List = code.split(",") + val userIdAndChatRoomId: List = keyAndValue[0].split(":") + return InvitationInfo(userIdAndChatRoomId[0], userIdAndChatRoomId[1], keyAndValue[1]) + } + + private fun encodeCode( key: String, value: String, ): String { val combinedString = "$key,$value" - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(combinedString.toByteArray(StandardCharsets.UTF_8)) - return Base64.getEncoder().encodeToString(hash) + val encodedString = Base64.getUrlEncoder().withoutPadding().encodeToString(combinedString.toByteArray(StandardCharsets.UTF_8)) + return encodedString } private fun generateKey( From 0ffce0cf7cdd0f544dfd480256ceae354cbb9438 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 15 Jun 2024 18:08:52 +0900 Subject: [PATCH 152/178] =?UTF-8?q?refac:=20String=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=9B=EB=8D=98=20type=20parameter=20ChatType=20enum?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/api/v1/ChatController.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt index 78b9dee3..8bbdd64f 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/api/v1/ChatController.kt @@ -40,7 +40,7 @@ class ChatController( @GetMapping("/chat") fun getChats( - @RequestParam("type") type: String, + @RequestParam("type") type: ChatType, @RequestParam("id") id: String, @RequestParam("page") page: Int, @RequestHeader("Authorization") token: String, @@ -48,15 +48,14 @@ class ChatController( val userId = authClient.getTokenInfo(token).data!!.userId val result = when (type) { - ChatType.Room.toString() -> chatService.getRoomChats(id, userId, page) - ChatType.Server.toString() -> + ChatType.Room -> chatService.getRoomChats(id, userId, page) + ChatType.Server -> chatService.getServerChats( id, userId, page, serverClient.getServerList(token, GetServerCondition()).body!!.data!!, ) - else -> throw GlobalException(ErrorCode.INVALID_CHAT_TYPE) } return ResponseEntity.ok().body(ApiResponse(data = result, status = 200)) } @@ -77,16 +76,15 @@ class ChatController( @DeleteMapping("/chat/{chatId}") fun deleteChat( - @RequestParam("type") type: String, + @RequestParam("type") type: ChatType, @PathVariable("chatId") chatId: String, @RequestHeader("Authorization") token: String, ): ResponseEntity<*> { val userId = authClient.getTokenInfo(token).data!!.userId val result = when (type) { - ChatType.Room.toString() -> chatService.deleteRoomChat(chatId, userId) - ChatType.Server.toString() -> chatService.deleteServerChat(chatId, userId) - else -> throw GlobalException(ErrorCode.INVALID_CHAT_TYPE) + ChatType.Room -> chatService.deleteRoomChat(chatId, userId) + ChatType.Server -> chatService.deleteServerChat(chatId, userId) } return ResponseEntity.ok().body(ApiResponse(status = 200)) } From 8f8031f0b372cb1b751e6cd47196878bb7f7d7ae Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 15 Jun 2024 19:34:55 +0900 Subject: [PATCH 153/178] =?UTF-8?q?refac:=20=EA=B2=80=EC=A6=9D=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20//Then=20=ED=9B=84=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chat/api/v1/ChatControllerTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt index dcc62d09..a665c1ca 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/api/v1/ChatControllerTest.kt @@ -96,6 +96,7 @@ class ChatControllerTest( .bodyValue(request) .exchange() + // Then val docs = result .expectStatus() @@ -103,7 +104,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(data = null, status = 201))) - // Then docs.restDoc( identifier = "create_chat_201", description = "채팅 생성 api", @@ -173,6 +173,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -180,7 +181,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(data = data))) - // Then docs.restDoc( identifier = "get_server_chats_200", description = "서버 채팅 조회 api", @@ -253,6 +253,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -260,7 +261,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(data = data))) - // Then docs.restDoc( identifier = "get_room_chats_200", description = "채팅방 채팅 조회 api", @@ -319,6 +319,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -326,7 +327,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(status = 200))) - // Then docs.restDoc( identifier = "update_chats_200", description = "채팅방 채팅 업데이트 api", @@ -381,6 +381,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -388,7 +389,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(status = 200))) - // Then docs.restDoc( identifier = "update_chats_200", description = "서버 채팅 업데이트 api", @@ -439,6 +439,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -446,7 +447,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(status = 200))) - // Then docs.restDoc( identifier = "delete_room_chat_200", description = "채팅방 채팅 삭제 api", @@ -499,6 +499,7 @@ class ChatControllerTest( .header("Authorization", "Bearer mock_token") .exchange() + // Then val docs = result .expectStatus() @@ -506,7 +507,6 @@ class ChatControllerTest( .expectBody() .json(om.writeValueAsString(ApiResponse(status = 200))) - // Then docs.restDoc( identifier = "delete_server_chat_200", description = "서버 채팅 삭제 api", From b37dddfd5fd47903f9d44319550d272b32afc684 Mon Sep 17 00:00:00 2001 From: minisun Date: Sat, 15 Jun 2024 20:18:42 +0900 Subject: [PATCH 154/178] =?UTF-8?q?refac:=20returns=20Unit=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20just=20Runs=20=EC=82=AC=EC=9A=A9=ED=95=B4=EC=84=9C?= =?UTF-8?q?=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EB=86=92=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index a6c7a873..20b95e4d 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -3,9 +3,7 @@ package kpring.chat.chat import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify +import io.mockk.* import kpring.chat.chat.model.Chat import kpring.chat.chat.repository.RoomChatRepository import kpring.chat.chat.repository.ServerChatRepository @@ -206,7 +204,7 @@ class ChatServiceTest( ) every { serverChatRepository.findById(chatId) } returns Optional.of(chat) - every { serverChatRepository.delete(chat) } returns Unit + every { serverChatRepository.delete(chat) } just Runs // When val result = chatService.deleteServerChat(chatId, userId) @@ -228,7 +226,7 @@ class ChatServiceTest( ) every { roomChatRepository.findById(chatId) } returns Optional.of(chat) - every { roomChatRepository.delete(chat) } returns Unit + every { roomChatRepository.delete(chat) } just Runs // When val result = chatService.deleteRoomChat(chatId, userId) From 3d1c8131641010578e7c2c257f512f460e5aa557 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:53:31 +0900 Subject: [PATCH 155/178] =?UTF-8?q?refac:=20id,members=20var=EC=97=90?= =?UTF-8?q?=EC=84=9C=20val=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt index 4c9e5782..5c3896d3 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/model/ChatRoom.kt @@ -5,12 +5,10 @@ import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document @Document(collection = "chatrooms") -class ChatRoom : BaseTime() { - @Id - var id: String? = null - - var members: MutableSet = mutableSetOf() - +class ChatRoom( + @Id val id: String? = null, + val members: MutableSet = mutableSetOf(), +) : BaseTime() { fun getUsers(): Set { return members } From 3b71575f19627b482ce50d7b15361346e363ac82 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:54:37 +0900 Subject: [PATCH 156/178] =?UTF-8?q?refac:=20createChatRoom=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=97=90=EC=84=9C=20Chat=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=8B=9C=20members=EC=97=90=20=EC=9A=94=EC=B2=AD=ED=95=98?= =?UTF-8?q?=EB=8A=94=20user=EC=9D=98=20id=EA=B0=80=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=EB=90=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index bd923a49..4abc7f0e 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -18,7 +18,7 @@ class ChatRoomService( request: CreateChatRoomRequest, userId: String, ) { - val chatRoom = ChatRoom() + val chatRoom = ChatRoom(members = mutableSetOf(userId)) chatRoom.addUsers(request.users) chatRoomRepository.save(chatRoom) } From 3cc8250ac4ba16c6b41bafe409534f0f96161f2d Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:54:57 +0900 Subject: [PATCH 157/178] =?UTF-8?q?refac:=20ChatRoomServiceTest=EC=97=90?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt index ebe938ef..4bc2f3cc 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/ChatRoomServiceTest.kt @@ -36,9 +36,10 @@ class ChatRoomServiceTest : FunSpec({ test("exitChatRoom 은 요청한 사람이 members의 일원이라면 삭제해야 한다") { // Given val chatRoom = - ChatRoom().apply { + ChatRoom( id = - ChatRoomTest.TEST_ROOM_ID + ChatRoomTest.TEST_ROOM_ID, + ).apply { addUsers(ChatRoomTest.TEST_MEMBERS) } every { chatRoomRepository.findById(chatRoom.id!!) } returns Optional.of(chatRoom) From ab1af1956d7d9706731d88f90620a167e2e37eed Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:55:09 +0900 Subject: [PATCH 158/178] =?UTF-8?q?refac:=20SampleTest=EC=97=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/test/kotlin/kpring/chat/example/SampleTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt b/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt index 9cf375e3..f9b70bfc 100644 --- a/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt +++ b/chat/src/test/kotlin/kpring/chat/example/SampleTest.kt @@ -28,7 +28,7 @@ class SampleTest( val chat = QChat.chat repeat(5) { idx -> roomChatRepository.save( - Chat("testUserId", "testRoomId", "testContent$idx"), + Chat(userId = "testUserId", contextId = "testRoomId", content = "testContent$idx"), ) } @@ -53,7 +53,7 @@ class SampleTest( roomChatRepository.deleteAll() repeat(5) { idx -> roomChatRepository.save( - Chat("testUserId", "testRoomId", "testContent$idx"), + Chat(userId = "testUserId", contextId = "testRoomId", content = "testContent$idx"), ) } From 97bfc370b0a10da5b2e6352fc6a337ec03267da6 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:55:51 +0900 Subject: [PATCH 159/178] =?UTF-8?q?refac:=20Chat=EC=9D=98=20id=20var?= =?UTF-8?q?=EC=97=90=EC=84=9C=20val=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt index dd11d349..0d295218 100644 --- a/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt +++ b/chat/src/main/kotlin/kpring/chat/chat/model/Chat.kt @@ -8,14 +8,13 @@ import org.springframework.data.mongodb.core.mapping.Document @NoArg @Document(collection = "chats") class Chat( + @Id + val id: String? = null, val userId: String, // roomId or serverId val contextId: String, var content: String, ) : BaseTime() { - @Id - var id: String? = null - fun isEdited(): Boolean { return !createdAt.equals(updatedAt) } From 25bb6a4a9b2bf65e4261b7bd83d6e1345260e31b Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 14:56:07 +0900 Subject: [PATCH 160/178] =?UTF-8?q?refac:=20ChatServiceTest=EC=97=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index c75112c2..b27c37a5 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -35,7 +35,8 @@ class ChatServiceTest( // Given val request = CreateChatRequest(id = ChatRoomTest.TEST_ROOM_ID, content = ChatTest.CONTENT, type = ChatType.Room) val userId = CommonTest.TEST_USER_ID - val roomChat = Chat(userId, request.id, request.content) + val chatId = ChatTest.TEST_CHAT_ID + val roomChat = Chat(chatId, userId, request.id, request.content) every { roomChatRepository.save(any()) } returns roomChat // When @@ -98,6 +99,7 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( + chatId, userId, roomId, "content", @@ -126,6 +128,7 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( + chatId, userId, serverId, "content", @@ -153,6 +156,7 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( + chatId, userId, roomId, "content", @@ -177,6 +181,7 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( + chatId, userId, serverId, "content", From d7e010d53b82e24c09f038e4099ddbecbf53539d Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 15:07:40 +0900 Subject: [PATCH 161/178] =?UTF-8?q?refac:=20code=EB=A5=BC=20decode?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20private?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chatroom/service/ChatRoomService.kt | 3 +-- .../kotlin/kpring/chat/chatroom/service/InvitationService.kt | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt index 4abc7f0e..b301ec2f 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/ChatRoomService.kt @@ -50,8 +50,7 @@ class ChatRoomService( code: String, userId: String, ): Boolean { - val decodedCode = invitationService.decodeCode(code) - val invitationInfo = invitationService.getInvitationInfoFromCode(decodedCode) + val invitationInfo = invitationService.getInvitationInfoFromCode(code) verifyInvitationExistence(invitationInfo) val chatRoom = getChatRoom(invitationInfo.chatRoomId) chatRoom.addUser(userId) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt index a4ecb187..80fd9837 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt @@ -39,14 +39,15 @@ class InvitationService( return encodeCode(key, code) } - fun decodeCode(encodedString: String): String { + private fun decodeCode(encodedString: String): String { val decodedBytes = Base64.getUrlDecoder().decode(encodedString) val decodedString = String(decodedBytes, StandardCharsets.UTF_8) return decodedString } fun getInvitationInfoFromCode(code: String): InvitationInfo { - val keyAndValue: List = code.split(",") + val decodedCode = decodeCode(code) + val keyAndValue: List = decodedCode.split(",") val userIdAndChatRoomId: List = keyAndValue[0].split(":") return InvitationInfo(userIdAndChatRoomId[0], userIdAndChatRoomId[1], keyAndValue[1]) } From 938d53b1aa7d660327b64b36a8b0d497e5710939 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 15:08:21 +0900 Subject: [PATCH 162/178] =?UTF-8?q?refac:=20getInvitationInfoFromCode=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/chat/chatroom/service/InvitationService.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt index 80fd9837..fef49d77 100644 --- a/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt +++ b/chat/src/main/kotlin/kpring/chat/chatroom/service/InvitationService.kt @@ -46,10 +46,10 @@ class InvitationService( } fun getInvitationInfoFromCode(code: String): InvitationInfo { - val decodedCode = decodeCode(code) - val keyAndValue: List = decodedCode.split(",") - val userIdAndChatRoomId: List = keyAndValue[0].split(":") - return InvitationInfo(userIdAndChatRoomId[0], userIdAndChatRoomId[1], keyAndValue[1]) + val decodedCode = decodeCode(code) // decode encoded code + val keyAndValue: List = decodedCode.split(",") // a code is consisted of key,value + val userIdAndChatRoomId: List = keyAndValue[0].split(":") // a key is consisted of userId:chatRoomId + return InvitationInfo(userId = userIdAndChatRoomId[0], chatRoomId = userIdAndChatRoomId[1], code = keyAndValue[1]) } private fun encodeCode( From 7098290c9355844a28a29bb3aebefc9ba76d78c8 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 15:46:14 +0900 Subject: [PATCH 163/178] =?UTF-8?q?chore=20:=20platform=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 6 ++++++ server/build.gradle.kts | 6 ++++++ user/build.gradle.kts | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 7d03df23..92ad5e63 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -82,6 +82,12 @@ openapi3 { jib { from { image = "eclipse-temurin:21-jre" + platforms { + platform { + architecture = "arm64" + os = "linux" + } + } } to { image = "youdong98/kpring-auth-application" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 3cb9a6cd..8c12cabd 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -79,6 +79,12 @@ openapi3 { jib { from { image = "eclipse-temurin:21-jre" + platforms { + platform { + architecture = "arm64" + os = "linux" + } + } } to { image = "youdong98/kpring-server-application" diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 8f89c009..595e5bee 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -63,6 +63,12 @@ openapi3 { jib { from { image = "eclipse-temurin:21-jre" + platforms { + platform { + architecture = "arm64" + os = "linux" + } + } } to { image = "youdong98/kpring-user-application" From 90051da12c5fa7e635e0163aea873fdba2655386 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 15:55:40 +0900 Subject: [PATCH 164/178] =?UTF-8?q?chore=20:=20cluster=20domain=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 2 +- server/build.gradle.kts | 2 +- user/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 92ad5e63..c00d3766 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -71,7 +71,7 @@ tasks.asciidoctor { } openapi3 { - setServer("http://localhost/auth") + setServer("http://144.24.59.91/auth") title = "Auth API" description = "API document" version = "0.1.0" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 8c12cabd..a32a19a6 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -68,7 +68,7 @@ kapt { } openapi3 { - setServer("http://localhost/server") + setServer("http://144.24.59.91/server") title = "Server API" description = "API document" version = "0.1.0" diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 595e5bee..c34a53d1 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { } openapi3 { - setServer("http://localhost/user") + setServer("http://144.24.59.91/user") title = "User API" description = "API document" version = "0.1.0" From d88ef663cf490450ba692dfd76d937fb7f3b0482 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 15:57:26 +0900 Subject: [PATCH 165/178] =?UTF-8?q?chore=20:=20=EA=B0=9C=EB=B0=9C=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=9D=84=20=EC=9C=84=ED=95=9C=20docker=20compose=20sw?= =?UTF-8?q?agger=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/infra/compose.yml b/infra/compose.yml index a25712f4..96d474a3 100644 --- a/infra/compose.yml +++ b/infra/compose.yml @@ -4,3 +4,11 @@ services: image: registry:latest ports: - "5000:5000" + + swagger: + image: swaggerapi/swagger-ui + ports: + - "8080:8080" + environment: + URLS: "[{name: 'auth', url: 'http://144.24.59.91/auth/static/openapi3.yaml'},{name: 'user', url: 'http://144.24.59.91/user/static/openapi3.yaml'}, {name: 'server', url: 'http://144.24.59.91/server/static/openapi3.yaml'}]" + From 031cf0f71049c31ada38efadeda161a0a500614e Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 15:58:17 +0900 Subject: [PATCH 166/178] =?UTF-8?q?chore=20:=20k8s=20swagger=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/charts/swagger/.helmignore | 23 ------- infra/charts/swagger/Chart.yaml | 9 --- infra/charts/swagger/templates/_helpers.tpl | 62 ------------------- .../charts/swagger/templates/deployment.yaml | 28 --------- infra/charts/swagger/templates/service.yaml | 16 ----- .../templates/tests/test-connection.yaml | 13 ---- infra/charts/swagger/values.yaml | 9 --- 7 files changed, 160 deletions(-) delete mode 100644 infra/charts/swagger/.helmignore delete mode 100644 infra/charts/swagger/Chart.yaml delete mode 100644 infra/charts/swagger/templates/_helpers.tpl delete mode 100644 infra/charts/swagger/templates/deployment.yaml delete mode 100644 infra/charts/swagger/templates/service.yaml delete mode 100644 infra/charts/swagger/templates/tests/test-connection.yaml delete mode 100644 infra/charts/swagger/values.yaml diff --git a/infra/charts/swagger/.helmignore b/infra/charts/swagger/.helmignore deleted file mode 100644 index 0e8a0eb3..00000000 --- a/infra/charts/swagger/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/infra/charts/swagger/Chart.yaml b/infra/charts/swagger/Chart.yaml deleted file mode 100644 index 0ac2a497..00000000 --- a/infra/charts/swagger/Chart.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v2 -name: swagger -description: swagger ui service - -type: application - -version: 0.1.0 - -appVersion: "1.16.0" diff --git a/infra/charts/swagger/templates/_helpers.tpl b/infra/charts/swagger/templates/_helpers.tpl deleted file mode 100644 index 55c38e84..00000000 --- a/infra/charts/swagger/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "swagger.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "swagger.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "swagger.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "swagger.labels" -}} -helm.sh/chart: {{ include "swagger.chart" . }} -{{ include "swagger.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "swagger.selectorLabels" -}} -app.kubernetes.io/name: {{ include "swagger.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "swagger.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "swagger.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/infra/charts/swagger/templates/deployment.yaml b/infra/charts/swagger/templates/deployment.yaml deleted file mode 100644 index 9563e632..00000000 --- a/infra/charts/swagger/templates/deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -{{- if eq .Values.global.profile "local" }} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.service.name }} - namespace: {{ .Release.namespace }} - labels: - app: {{ .Values.service.name }} -spec: - replicas: 1 - selector: - matchLabels: - app: {{ .Values.service.name }} - template: - metadata: - labels: - app: {{ .Values.service.name }} - spec: - containers: - - name: {{ .Values.service.name }} - image: {{ .Values.service.image }} - ports: - - containerPort: {{ .Values.service.port }} - env: - - name: URLS - value: | - {{ .Values.swagger.urls }} -{{ end }} \ No newline at end of file diff --git a/infra/charts/swagger/templates/service.yaml b/infra/charts/swagger/templates/service.yaml deleted file mode 100644 index d06e7c01..00000000 --- a/infra/charts/swagger/templates/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{- if eq .Values.global.profile "local" }} -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.service.name }} - namespace: {{ .Release.namespace }} -spec: - type: NodePort - ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.port }} - nodePort: {{ .Values.service.nodePort }} - protocol: TCP - selector: - app: {{ .Values.service.name }} -{{- end }} \ No newline at end of file diff --git a/infra/charts/swagger/templates/tests/test-connection.yaml b/infra/charts/swagger/templates/tests/test-connection.yaml deleted file mode 100644 index 5023d861..00000000 --- a/infra/charts/swagger/templates/tests/test-connection.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ .Values.service.name }}-test-connection" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ .Values.service.name }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/infra/charts/swagger/values.yaml b/infra/charts/swagger/values.yaml deleted file mode 100644 index 35e377cb..00000000 --- a/infra/charts/swagger/values.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# swagger ui service -service: - name: swagger-ui - image: swaggerapi/swagger-ui - port: 8080 - nodePort: 30000 - -swagger: - urls: "[{name: 'auth', url: 'http://localhost/auth/static/openapi3.yaml'},{name: 'user', url: 'http://localhost/user/static/openapi3.yaml'}, {name: 'server', url: 'http://localhost/server/static/openapi3.yaml'}]" From b2cce9c79738962ff48de9e305e371beb35f2340 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 16:09:11 +0900 Subject: [PATCH 167/178] =?UTF-8?q?feat:=20joinChatRoom=20Controller=20Lay?= =?UTF-8?q?er=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatroom/api/v1/ChatRoomControllerTest.kt | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt index e157c620..09dd27d0 100644 --- a/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chatroom/api/v1/ChatRoomControllerTest.kt @@ -81,9 +81,9 @@ class ChatRoomControllerTest( // When val result = webTestClient.get().uri(url, chatRoomId).header("Authorization", "Bearer mock_token").exchange() + // Then val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(data = data))) - // Then docs.restDoc( identifier = "getChatRoomInvitation_200", description = "채팅방 참여코드를 위한 key값을 반환하는 api", @@ -102,4 +102,55 @@ class ChatRoomControllerTest( } } } + + describe("GET /api/v1//chatroom/{code}/join : joinChatRoom api test") { + + val url = "/api/v1/chatroom/{code}/join" + it("joinChatRoom api test") { + + // Given + val chatRoomId = ChatRoomTest.TEST_ROOM_ID + val userId = CommonTest.TEST_USER_ID + val code = "666fcd76027b2432e4b49a0f" + val data = true + + every { authClient.getTokenInfo(any()) } returns + ApiResponse( + data = + TokenInfo( + type = TokenType.ACCESS, userId = CommonTest.TEST_USER_ID, + ), + ) + + every { + chatRoomService.joinChatRoom( + code, + userId, + ) + } returns data + + // When + val result = webTestClient.patch().uri(url, code).header("Authorization", "Bearer mock_token").exchange() + + // Then + val docs = result.expectStatus().isOk.expectBody().json(om.writeValueAsString(ApiResponse(status = 200))) + + docs.restDoc( + identifier = "joinChatRoom_200", + description = "코드로 채팅방에 참여하는 api", + ) { + request { + path { + "code" mean "채팅방 참여코드" + } + } + + response { + body { + "status" type JsonDataType.Integers mean "상태 코드" + } + } + } + } + } }) From 62b9e32e09a9abcd459f4b469f27de70a47e46f5 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 17:24:45 +0900 Subject: [PATCH 168/178] =?UTF-8?q?chore=20:=20hostname=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 4 +++- server/build.gradle.kts | 4 +++- user/build.gradle.kts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index c00d3766..00d62a3a 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -70,8 +70,10 @@ tasks.asciidoctor { dependsOn(tasks.test) } +val hostname = "152.70.145.249" + openapi3 { - setServer("http://144.24.59.91/auth") + setServer("http://$hostname/auth") title = "Auth API" description = "API document" version = "0.1.0" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index a32a19a6..04969f8c 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -67,8 +67,10 @@ kapt { annotationProcessor("org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor") } +val hostname = "152.70.145.249" + openapi3 { - setServer("http://144.24.59.91/server") + setServer("http://$hostname/server") title = "Server API" description = "API document" version = "0.1.0" diff --git a/user/build.gradle.kts b/user/build.gradle.kts index c34a53d1..4b861640 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -51,8 +51,10 @@ dependencies { implementation("org.springframework.restdocs:spring-restdocs-asciidoctor") } +val hostname = "152.70.145.249" + openapi3 { - setServer("http://144.24.59.91/user") + setServer("http://$hostname/user") title = "User API" description = "API document" version = "0.1.0" From 7db945345decd7b5b57fe24264d42b0cbe78b491 Mon Sep 17 00:00:00 2001 From: minisun Date: Mon, 17 Jun 2024 17:39:57 +0900 Subject: [PATCH 169/178] =?UTF-8?q?refac:=20ChatServiceTest=EC=97=90=20Cha?= =?UTF-8?q?t=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kpring/chat/chat/ChatServiceTest.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt index c9f0f225..b28b82c5 100644 --- a/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt +++ b/chat/src/test/kotlin/kpring/chat/chat/ChatServiceTest.kt @@ -203,9 +203,10 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( - userId, - serverId, - "content", + id = chatId, + userId = userId, + contextId = serverId, + content = "content", ) every { serverChatRepository.findById(chatId) } returns Optional.of(chat) @@ -225,9 +226,10 @@ class ChatServiceTest( val userId = CommonTest.TEST_USER_ID val chat = Chat( - userId, - roomId, - "content", + id = chatId, + userId = userId, + contextId = roomId, + content = "content", ) every { roomChatRepository.findById(chatId) } returns Optional.of(chat) From f29cead846560b639a39af9691e4bec150999f9a Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Mon, 17 Jun 2024 20:27:07 +0900 Subject: [PATCH 170/178] =?UTF-8?q?chore=20:=20docker=20compose=20ip=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/compose.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/compose.yml b/infra/compose.yml index 96d474a3..f04d004c 100644 --- a/infra/compose.yml +++ b/infra/compose.yml @@ -10,5 +10,4 @@ services: ports: - "8080:8080" environment: - URLS: "[{name: 'auth', url: 'http://144.24.59.91/auth/static/openapi3.yaml'},{name: 'user', url: 'http://144.24.59.91/user/static/openapi3.yaml'}, {name: 'server', url: 'http://144.24.59.91/server/static/openapi3.yaml'}]" - + URLS: "[{name: 'auth', url: 'http://152.70.145.249/auth/static/openapi3.yaml'},{name: 'user', url: 'http://152.70.145.249/user/static/openapi3.yaml'}, {name: 'server', url: 'http://152.70.145.249/server/static/openapi3.yaml'}]" From 7d3e84491142a70bc9fba84b745c5b4d729bef36 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 11:29:25 +0900 Subject: [PATCH 171/178] =?UTF-8?q?chore=20:=20github=20actions=20ci=20job?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3b07a606 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [ dev ] # push 되었을 때, 실행 + +jobs: + cd: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # jdk 21 환경 구성 + - name: set up jdk 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '21' + + # Gradle wrapper 파일 실행 권한주기 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Gradle test를 실행한다 + - name: update image using jib + run: ./gradlew --info jib From 2aa1d55172718aacb5917a43d8bbe466d2349a68 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 11:31:33 +0900 Subject: [PATCH 172/178] =?UTF-8?q?chore=20:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EC=B6=94=EC=A0=81=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20tag=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 2 +- server/build.gradle.kts | 2 +- user/build.gradle.kts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 00d62a3a..4b007340 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -94,7 +94,7 @@ jib { to { image = "youdong98/kpring-auth-application" setAllowInsecureRegistries(true) - tags = setOf("latest") + tags = setOf("latest", System.nanoTime().toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 04969f8c..e5393b57 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -91,7 +91,7 @@ jib { to { image = "youdong98/kpring-server-application" setAllowInsecureRegistries(true) - tags = setOf("latest") + tags = setOf("latest", System.nanoTime().toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 4b861640..70d29a9f 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -75,7 +75,7 @@ jib { to { image = "youdong98/kpring-user-application" setAllowInsecureRegistries(true) - tags = setOf("latest") + tags = setOf("latest", System.nanoTime().toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") From eaebb816a78e497195d5fcc6acdfda34f235841a Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 14:58:48 +0900 Subject: [PATCH 173/178] =?UTF-8?q?chore:=20git=20commit=20hash=EB=A5=BC?= =?UTF-8?q?=20=ED=86=B5=ED=95=9C=20=EB=B2=84=EC=A0=84=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 2 +- build.gradle.kts | 25 ++++++++++++++++++- server/build.gradle.kts | 2 +- .../kotlin/kpring/server/util/DtoMapper.kt | 2 +- user/build.gradle.kts | 2 +- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 4b007340..d1da7fcd 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -94,7 +94,7 @@ jib { to { image = "youdong98/kpring-auth-application" setAllowInsecureRegistries(true) - tags = setOf("latest", System.nanoTime().toString()) + tags = setOf("latest", version.toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") diff --git a/build.gradle.kts b/build.gradle.kts index c700c98d..1fabda5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.tasks.bundling.BootJar +import java.io.IOException plugins { id("org.springframework.boot") version "3.2.4" @@ -17,7 +18,7 @@ repositories { allprojects { group = "com.sideproject" - version = "0.0.1-SNAPSHOT" + version = "git rev-parse --short=8 HEAD".runCommand(workingDir = rootDir) repositories { mavenCentral() @@ -53,3 +54,25 @@ subprojects { debug.set(true) } } + +/** + * cli 실행 결과를 반환한기 위한 함수 + */ +fun String.runCommand( + workingDir: File = File("."), + timeoutAmount: Long = 60, + timeoutUnit: TimeUnit = TimeUnit.SECONDS, +): String = + ProcessBuilder(split("\\s(?=(?:[^'\"`]*(['\"`])[^'\"`]*\\1)*[^'\"`]*$)".toRegex())) + .directory(workingDir) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + .start() + .apply { waitFor(timeoutAmount, timeoutUnit) } + .run { + val error = errorStream.bufferedReader().readText().trim() + if (error.isNotEmpty()) { + throw IOException(error) + } + inputStream.bufferedReader().readText().trim() + } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index e5393b57..8cc6886f 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -91,7 +91,7 @@ jib { to { image = "youdong98/kpring-server-application" setAllowInsecureRegistries(true) - tags = setOf("latest", System.nanoTime().toString()) + tags = setOf("latest", version.toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") diff --git a/server/src/main/kotlin/kpring/server/util/DtoMapper.kt b/server/src/main/kotlin/kpring/server/util/DtoMapper.kt index 244192cf..ef37b0ea 100644 --- a/server/src/main/kotlin/kpring/server/util/DtoMapper.kt +++ b/server/src/main/kotlin/kpring/server/util/DtoMapper.kt @@ -5,7 +5,7 @@ import kpring.core.server.dto.ServerThemeInfo import kpring.server.domain.Category import kpring.server.domain.Theme -fun Category.toInfo(): CategoryInfo { +fun Category.toInfo(): CategoryInfo { return CategoryInfo( id = this.name, name = this.toString(), diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 70d29a9f..97628071 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -75,7 +75,7 @@ jib { to { image = "youdong98/kpring-user-application" setAllowInsecureRegistries(true) - tags = setOf("latest", System.nanoTime().toString()) + tags = setOf("latest", version.toString()) } container { jvmFlags = listOf("-Xms512m", "-Xmx512m") From 4eb830c457def6b256fd114d75d45b2d23e3596f Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 15:01:59 +0900 Subject: [PATCH 174/178] =?UTF-8?q?chore=20:=20ip=20->=20hostname=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/build.gradle.kts | 2 +- infra/compose.yml | 2 +- server/build.gradle.kts | 2 +- user/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index d1da7fcd..93c5f609 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -70,7 +70,7 @@ tasks.asciidoctor { dependsOn(tasks.test) } -val hostname = "152.70.145.249" +val hostname = "kpring.duckdns.org" openapi3 { setServer("http://$hostname/auth") diff --git a/infra/compose.yml b/infra/compose.yml index f04d004c..623d1962 100644 --- a/infra/compose.yml +++ b/infra/compose.yml @@ -10,4 +10,4 @@ services: ports: - "8080:8080" environment: - URLS: "[{name: 'auth', url: 'http://152.70.145.249/auth/static/openapi3.yaml'},{name: 'user', url: 'http://152.70.145.249/user/static/openapi3.yaml'}, {name: 'server', url: 'http://152.70.145.249/server/static/openapi3.yaml'}]" + URLS: "[{name: 'auth', url: 'http://kpring.duckdns.org/auth/static/openapi3.yaml'},{name: 'user', url: 'http://kpring.duckdns.org/user/static/openapi3.yaml'}, {name: 'server', url: 'http://kpring.duckdns.org/server/static/openapi3.yaml'}]" diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 8cc6886f..c2fd02cc 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -67,7 +67,7 @@ kapt { annotationProcessor("org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor") } -val hostname = "152.70.145.249" +val hostname = "kpring.duckdns.org" openapi3 { setServer("http://$hostname/server") diff --git a/user/build.gradle.kts b/user/build.gradle.kts index 97628071..3c49ed7d 100644 --- a/user/build.gradle.kts +++ b/user/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation("org.springframework.restdocs:spring-restdocs-asciidoctor") } -val hostname = "152.70.145.249" +val hostname = "kpring.duckdns.org" openapi3 { setServer("http://$hostname/user") From 68382ffe02e2d8bd111fab138a22026ffa362567 Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 15:04:16 +0900 Subject: [PATCH 175/178] =?UTF-8?q?chore=20:=20main=20push=EC=8B=9C?= =?UTF-8?q?=EC=97=90=20ci=20=EC=9E=91=EC=97=85=EC=9D=B4=20=EC=9E=91?= =?UTF-8?q?=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b07a606..54c1987c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ dev ] # push 되었을 때, 실행 + branches: [ main ] # push 되었을 때, 실행 jobs: cd: From c3d218ac5a3bc46b858404689878d9c824a5f070 Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 18 Jun 2024 15:22:21 +0900 Subject: [PATCH 176/178] =?UTF-8?q?refac:=20friendRequests=20immutable=20?= =?UTF-8?q?=ED=95=9C=20=ED=83=80=EC=9E=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kpring/user/dto/response/GetFriendRequestsResponse.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt index 1a8329c4..879ef532 100644 --- a/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt +++ b/user/src/main/kotlin/kpring/user/dto/response/GetFriendRequestsResponse.kt @@ -2,5 +2,5 @@ package kpring.user.dto.response data class GetFriendRequestsResponse( val userId: Long, - var friendRequests: MutableList, + var friendRequests: List, ) From e4ee4fad8358b4d6869326ac583a1dd35ff3fe26 Mon Sep 17 00:00:00 2001 From: minahYu Date: Tue, 18 Jun 2024 15:29:35 +0900 Subject: [PATCH 177/178] =?UTF-8?q?refac:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=B4=20stream=20api=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/kotlin/kpring/user/service/FriendServiceImpl.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt index eb0a4672..d1d7e16a 100644 --- a/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt +++ b/user/src/main/kotlin/kpring/user/service/FriendServiceImpl.kt @@ -9,6 +9,7 @@ import kpring.user.exception.UserErrorCode import kpring.user.repository.FriendRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.util.stream.Collectors @Service @Transactional @@ -21,10 +22,10 @@ class FriendServiceImpl( friendRepository.findAllByUserIdAndRequestStatus(userId, FriendRequestStatus.RECEIVED) val friendRequests: MutableList = mutableListOf() - for (friendRelation in friendRelations) { + friendRelations.stream().map { friendRelation -> val friend = friendRelation.friend - friendRequests.add(GetFriendRequestResponse(friend.id!!, friend.username)) - } + GetFriendRequestResponse(friend.id!!, friend.username) + }.collect(Collectors.toList()) return GetFriendRequestsResponse(userId, friendRequests) } From 3627418b86746432ae51918ab3fbbaf2f629e73e Mon Sep 17 00:00:00 2001 From: yudonggeun Date: Tue, 18 Jun 2024 16:14:17 +0900 Subject: [PATCH 178/178] =?UTF-8?q?docs=20:=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54c1987c..3a175800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,6 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - # Gradle test를 실행한다 + # Gradle jib를 통한 이미지 배포 - name: update image using jib run: ./gradlew --info jib