Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user #2

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b8ad1fa
feat: controller, service ์ƒ์„ฑ
23tae Aug 13, 2024
e3be647
chore: ์˜์กด์„ฑ ์ฃผ์ž…
23tae Aug 13, 2024
2cd815d
chore: ์‘๋‹ต์šฉ ์œ ์ € ์ •๋ณด dto
23tae Aug 20, 2024
6575451
chore: verificationMethod ํ•„๋“œ ์ถ”๊ฐ€
23tae Aug 20, 2024
bd2b2dc
chore: param DTO ์ž‘์„ฑ
23tae Aug 20, 2024
00a94e2
feat: ์ž์‹ ์˜ ํ”„๋กœํ•„ ์ •๋ณด ์กฐํšŒ
23tae Aug 25, 2024
c889057
Merge remote-tracking branch 'origin/feat/redis' into feat/user
23tae Aug 25, 2024
046464c
build: redis ์„ค์ •
23tae Aug 25, 2024
01dffdd
feat: id๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ๋Š” ๋ฉ”์„œ๋“œ ์ •์˜
23tae Aug 25, 2024
8847a0d
chore: ์˜ˆ์™ธ ์ •์˜
23tae Aug 25, 2024
2d516e2
chore: ํƒˆํ‡ด์ผ์‹œ ํ•„๋“œ ์ถ”๊ฐ€
23tae Aug 25, 2024
73c5f25
chore: userId๊ฐ€ null์ธ ๊ฒฝ์šฐ ์—๋Ÿฌ์ฝ”๋“œ ์ถ”๊ฐ€
23tae Aug 26, 2024
5cf6355
feat: ์œ ์ € ํ”„๋กœํ•„ ์กฐํšŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
23tae Aug 26, 2024
4e54a5d
chore: jpa ๋ฐ˜ํ™˜๊ฐ’ ์ˆ˜์ •
23tae Aug 26, 2024
542ddd9
feat: user entity๋ฅผ Dto์— ๋งคํ•‘
23tae Aug 26, 2024
117cd82
feat: ์œ ์ € ํ”„๋กœํ•„ ์กฐํšŒ API
23tae Aug 26, 2024
196f2c0
style: ktfmt ์ ์šฉ
23tae Aug 26, 2024
e71cef8
chore: database ๊ด€๋ จ ์„ค์ •
23tae Aug 27, 2024
ae0a7e0
fix: Redis ์‚ฌ์šฉ ๋ฐฉ์‹ ๋ณ€๊ฒฝ
23tae Aug 28, 2024
54ab978
merge main into feat/user
23tae Sep 26, 2024
5b057bf
fix: ํ† ํฐ์—์„œ userId ์‚ฌ์šฉ ๋ฐฉ์‹ ๋ณ€๊ฒฝ
23tae Sep 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dependencies {
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
developmentOnly("org.springframework.boot:spring-boot-devtools")
implementation("org.redisson:redisson-spring-boot-starter:3.18.0")
implementation("org.springframework.boot:spring-boot-starter-cache")

// Kotlin
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package uoslife.springaccount.app.user.controller

import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import uoslife.springaccount.app.user.dto.response.UserProfileDto
import uoslife.springaccount.app.user.service.UserService
import uoslife.springaccount.common.security.annotation.UserId

@RestController
@RequestMapping("/v2/user")
class UserController(private val userService: UserService) {

@GetMapping("/me")
fun getMyProfile(
@UserId userId: Long
): ResponseEntity<UserProfileDto.UserProfileResponse> {
return ResponseEntity.ok(userService.getProfile(userId))
}

@GetMapping("/{userId}")
fun getProfile(
@PathVariable userId: Long,
): ResponseEntity<UserProfileDto.UserProfileResponse> {
return ResponseEntity.ok(userService.getProfile(userId))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package uoslife.springaccount.app.user.domain.entity

import jakarta.persistence.*
import java.time.LocalDateTime
import uoslife.springaccount.app.device.domain.entity.Device
import uoslife.springaccount.app.identity.domain.entity.Identity
import uoslife.springaccount.app.moderator.domain.entity.Moderators
Expand Down Expand Up @@ -40,6 +41,8 @@ class User(
var avatarUrl: String? = avatarUrl
protected set

@Column(name = "deleted_at") var deletedAt: LocalDateTime? = null

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
protected val mutableDevices: MutableList<Device> = mutableListOf()
val devices: List<Device>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ package uoslife.springaccount.app.user.domain.repository.jpa
import org.springframework.data.jpa.repository.JpaRepository
import uoslife.springaccount.app.user.domain.entity.User

interface UserRepository : JpaRepository<User, Long>
interface UserRepository : JpaRepository<User, Long> {
fun findByIdAndDeletedAtIsNull(id: Long): User?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package uoslife.springaccount.app.user.dto.param

data class CreateUserDto(val nickname: String, val phoneNumber: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package uoslife.springaccount.app.user.dto.param

data class UpdateUserDto(val nickname: String?)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package uoslife.springaccount.app.user.dto.param

import uoslife.springaccount.app.identity.domain.entity.Identity
import uoslife.springaccount.app.moderator.domain.entity.Moderators
import uoslife.springaccount.app.verification.domain.entity.PortalAccounts
import uoslife.springaccount.app.verification.domain.entity.Verifications

data class UserWithInfoDto(
val identities: List<Identity>,
val portalAccounts: List<PortalAccounts>,
val verifications: List<Verifications>,
val moderator: Moderators?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package uoslife.springaccount.app.user.dto.response

import uoslife.springaccount.app.user.domain.entity.User
import uoslife.springaccount.app.verification.domain.entity.VerificationMethod
import uoslife.springaccount.common.error.ErrorCode
import uoslife.springaccount.common.error.baseexception.BusinessException

class UserProfileDto {

data class UserRealm(
val code: String,
val name: String,
)

data class UserProfileIdentity(
val id: String,
val type: String,
val status: String,
val idNumber: String,
val university: String?,
val department: String?,
val major: String?,
)

data class UserProfileModerator(
val generation: String,
val chapter: String,
val role: String,
)

data class UserProfileResponse(
val id: Long,
val nickname: String,
val phone: String,
val name: String?,
val email: String?,
// val realm: UserRealm?,
val identity: UserProfileIdentity?,
val moderator: UserProfileModerator?,
val isLinkedPortal: Boolean,
val isVerified: Boolean,
val verificationMethod: VerificationMethod?,
)

companion object {
fun toUserProfileResponse(user: User): UserProfileResponse {
return UserProfileResponse(
id = user.id ?: throw BusinessException(ErrorCode.USER_ID_NULL),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BusinessException์— ErrorCode๋ฅผ ๋„ฃ๋Š” ๊ฒƒ๋ณด๋‹ค BusinessException ๊ฐ์ฒด๋ฅผ ์ƒ์†ํ•œ UserNullException ๊ฐ™์€ ๊ฑธ ๋งŒ๋“ค์–ด ์“ฐ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค

nickname = user.nickname ?: "No Nickname",
phone = user.phoneNumber,
name = user.name,
email = user.email,
identity =
user.identities.firstOrNull()?.let { identity ->
UserProfileIdentity(
id = identity.id,
type = identity.type,
status = identity.status,
idNumber = identity.idNumber,
university = identity.university,
department = identity.department,
major = identity.major
)
},
moderator =
user.moderators.firstOrNull()?.let {
UserProfileModerator(
generation = it.generation,
chapter = it.chapter,
role = it.role
)
},
isLinkedPortal = user.portalAccounts.isNotEmpty(),
isVerified = user.verifications.isNotEmpty(),
verificationMethod = user.verifications.firstOrNull()?.method
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package uoslife.springaccount.app.user.service

import java.util.concurrent.TimeUnit
import org.redisson.api.RBucket
import org.redisson.api.RedissonClient
import org.springframework.stereotype.Service
import uoslife.springaccount.app.user.domain.repository.jpa.UserRepository
import uoslife.springaccount.app.user.dto.response.UserProfileDto
import uoslife.springaccount.app.user.util.UserConfig
import uoslife.springaccount.common.error.user.UserNotFoundException

@Service
class UserService(
private val userRepository: UserRepository,
private val redisClient: RedissonClient,
) {

fun getProfile(userId: Long): UserProfileDto.UserProfileResponse {
val cacheKey = getProfileCacheKey(userId)

// ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
val bucket: RBucket<UserProfileDto.UserProfileResponse> = redisClient.getBucket(cacheKey)
val cachedData = bucket.get()
if (cachedData != null) {
return cachedData
}

// DB์—์„œ ์œ ์ € ์กฐํšŒ
val user =
userRepository.findByIdAndDeletedAtIsNull(userId) ?: throw UserNotFoundException()

// User ์—”ํ‹ฐํ‹ฐ๋ฅผ DTO๋กœ ๋ณ€ํ™˜
val userProfileResponse = UserProfileDto.toUserProfileResponse(user)

// ์บ์‹œ ์ €์žฅ
bucket.set(userProfileResponse, UserConfig.USER_PROFILE_CACHE_TTL, TimeUnit.SECONDS)

return userProfileResponse
}

private fun getProfileCacheKey(userId: Long): String {
return "uoslife:user:profile:$userId"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package uoslife.springaccount.app.user.util

class UserConfig {
companion object {
const val USER_PROFILE_CACHE_TTL: Long = 60 * 60 // ์œ ์ € ์ •๋ณด ์ €์žฅ ์‹œ๊ฐ„. 1์‹œ๊ฐ„
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ enum class ErrorCode(val code: String, val message: String, var status: Int) {

// Auth
INVALID_TOKEN_VALUE("A01", "Invalid Token Value.", HttpStatus.BAD_REQUEST.value()),
TOKEN_EXPIRED_ERROR("A02", "Expired Token.", HttpStatus.UNAUTHORIZED.value())
TOKEN_EXPIRED_ERROR("A02", "Expired Token.", HttpStatus.UNAUTHORIZED.value()),

// User
USER_NOT_FOUND("U01", "User Not Found.", HttpStatus.NOT_FOUND.value()),
USER_ID_NULL("U02", "User ID is Null", HttpStatus.INTERNAL_SERVER_ERROR.value()),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package uoslife.springaccount.common.error.user

import uoslife.springaccount.common.error.ErrorCode
import uoslife.springaccount.common.error.baseexception.EntityNotFoundException

class UserNotFoundException : EntityNotFoundException(ErrorCode.USER_NOT_FOUND) {}
20 changes: 20 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ spring:
version: 1.0.1
banner:
location: classpath:/app-banner.dat
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
datasource:
url: jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DATABASE}?stringtype=unspecified
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
database-platform: org.hibernate.dialect.PostgreSQLDialect
generate-ddl: false
open-in-view: false
properties:
hibernate:
default_batch_fetch_size: 1000
show_sql: true
format_sql: true

security:
jwt:
Expand Down