Skip to content

Commit

Permalink
Feat/#91/endermaru - Post에 isBookmarked 추가 (#92)
Browse files Browse the repository at this point in the history
* nullable한 userResolver 추가

* UserOrNullResolver 추가 완료

* Post Service 수정
  • Loading branch information
endermaru authored Jan 29, 2025
1 parent ddf187d commit 3615fc9
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/main/kotlin/com/waffletoy/team1server/config/WebConfig.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.waffletoy.team1server.config

import com.waffletoy.team1server.user.UserArgumentResolver
import com.waffletoy.team1server.user.UserOrNullArgumentResolver
import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig(
private val userArgumentResolver: UserArgumentResolver,
private val userOrNullArgumentResolver: UserOrNullArgumentResolver,
) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(userArgumentResolver)
resolvers.add(userOrNullArgumentResolver)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.waffletoy.team1server.post.dto.PostBrief
import com.waffletoy.team1server.post.service.PostService
import com.waffletoy.team1server.post.service.S3Service
import com.waffletoy.team1server.user.AuthUser
import com.waffletoy.team1server.user.AuthUserOrNull
import com.waffletoy.team1server.user.dtos.User
import io.swagger.v3.oas.annotations.Parameter
import jakarta.validation.constraints.Max
Expand All @@ -26,23 +27,27 @@ class PostController(
// 채용 공고 상세 페이지 불러오기
@GetMapping("/{post_id}")
fun getPageDetail(
// User 토큰이 들어올 수도, 아닐 수도 있음
@Parameter(hidden = true) @AuthUserOrNull user: User?,
@PathVariable("post_id") postId: String,
): ResponseEntity<Post> {
val post = postService.getPageDetail(postId)
val post = postService.getPageDetail(user, postId)
return ResponseEntity.ok(post)
}

// 채용 공고 리스트 불러오기
@GetMapping
fun getPosts(
// User 토큰이 들어올 수도, 아닐 수도 있음
@Parameter(hidden = true) @AuthUserOrNull user: User?,
@RequestParam(required = false) roles: List<String>?,
@RequestParam(required = false) @Min(0) investmentMax: Int?,
@RequestParam(required = false) @Min(0) investmentMin: Int?,
@RequestParam(required = false) @Min(0) @Max(2) status: Int?,
@RequestParam(required = false) series: List<String>?,
@RequestParam(required = false) @Min(0) page: Int?,
): ResponseEntity<PostWithPage> {
val posts = postService.getPosts(roles, investmentMax, investmentMin, status, series, page ?: 0)
val posts = postService.getPosts(user, roles, investmentMax, investmentMin, status, series, page ?: 0)

// 총 페이지
val totalPages = posts.totalPages
Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/com/waffletoy/team1server/post/dto/Post.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ data class Post(
val category: Category,
val detail: String,
val headcount: String,
// 북마크 여부
val isBookmarked: Boolean = false,
) {
companion object {
fun fromEntity(entity: PositionEntity): Post =
fun fromEntity(
entity: PositionEntity,
isBookmarked: Boolean = false,
): Post =
Post(
id = entity.id,
author =
Expand Down Expand Up @@ -64,6 +69,7 @@ data class Post(
category = entity.category,
detail = entity.detail ?: "",
headcount = entity.headcount,
isBookmarked = isBookmarked,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class PostBrief(
val isActive: Boolean,
val category: Category,
val headcount: String,
val isBookmarked: Boolean,
) {
companion object {
fun fromPost(post: Post): PostBrief =
Expand All @@ -41,6 +42,7 @@ data class PostBrief(
isActive = post.isActive,
category = post.category,
headcount = post.headcount,
isBookmarked = post.isBookmarked,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface BookmarkRepository : JpaRepository<BookmarkEntity, String> {
position: PositionEntity,
): BookmarkEntity?

fun findByUser(user: UserEntity): List<BookmarkEntity>

@Query("SELECT b.position FROM BookmarkEntity b WHERE b.user = :user")
fun findPositionsByUser(
@Param("user") user: UserEntity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.waffletoy.team1server.post.Series
import com.waffletoy.team1server.post.dto.Post
import com.waffletoy.team1server.post.dto.TagVo
import com.waffletoy.team1server.post.persistence.*
import com.waffletoy.team1server.user.dtos.User
import com.waffletoy.team1server.user.persistence.*
import com.waffletoy.team1server.user.service.UserService
import org.springframework.beans.factory.annotation.Value
Expand All @@ -30,19 +31,28 @@ class PostService(
/**
* Retrieves detailed information of a specific post by its ID.
*
* @param user nullable user field to get bookmark
* @param postId The unique identifier of the post.
* @return The detailed [Post] object.
* @throws PostNotFoundException If the post with the given ID does not exist.
*/
@Transactional(readOnly = true)
fun getPageDetail(postId: String): Post {
fun getPageDetail(
user: User?,
postId: String,
): Post {
val positionEntity = getPositionEntityOrThrow(postId)
return Post.fromEntity(positionEntity)
val bookmarkIds = getBookmarkIds(user)
return Post.fromEntity(
entity = positionEntity,
isBookmarked = positionEntity.id in bookmarkIds,
)
}

/**
* Retrieves a paginated list of posts based on provided filters.
*
* @param user nullable user field to get bookmark
* @param positions List of position names to filter by.
* @param investmentMax Maximum investment amount.
* @param investmentMin Minimum investment amount.
Expand All @@ -54,6 +64,7 @@ class PostService(
*/
@Transactional(readOnly = true)
fun getPosts(
user: User?,
positions: List<String>?,
investmentMax: Int?,
investmentMin: Int?,
Expand Down Expand Up @@ -84,7 +95,15 @@ class PostService(
val validPage = if (page < 0) 0 else page
val pageable = PageRequest.of(validPage, pageSize)
val positionPage = positionRepository.findAll(specification, pageable)
return positionPage.map { position -> Post.fromEntity(position) }

val bookmarkIds = getBookmarkIds(user)

return positionPage.map { position ->
Post.fromEntity(
entity = position,
isBookmarked = position.id in bookmarkIds,
)
}
}

/**
Expand Down Expand Up @@ -163,7 +182,12 @@ class PostService(
val validPage = if (page < 0) 0 else page
val pageable = PageRequest.of(validPage, pageSize)
val positionPage = bookmarkRepository.findPositionsByUser(userEntity, pageable)
return positionPage.map { position -> Post.fromEntity(position) }
return positionPage.map { position ->
Post.fromEntity(
entity = position,
isBookmarked = true,
)
}
}

/**
Expand Down Expand Up @@ -241,6 +265,19 @@ class PostService(

fun getPositionEntityByPostId(postId: String): PositionEntity? = positionRepository.findByIdOrNull(postId)

fun getBookmarkIds(user: User?): Set<String> {
if (user == null) {
return emptySet()
}

val userEntity = userService.getUserEntityByUserId(user.id)
return if (userEntity != null) {
bookmarkRepository.findByUser(userEntity).map { it.position.id }.toSet()
} else {
emptySet()
}
}

@Value("\${custom.SECRET}")
private lateinit var resetDbSecret: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ data class Resume(
companion object {
fun fromEntity(
resumeEntity: ResumeEntity,
includeAuthor: Boolean = true,
// includeAuthor: Boolean = true,
): Resume =
Resume(
id = resumeEntity.id,
positionTitle = resumeEntity.position?.title,
companyName = resumeEntity.position?.company?.companyName,
author = if (includeAuthor) User.fromEntity(resumeEntity.user, includeResumes = false) else null,
// author = if (includeAuthor) User.fromEntity(resumeEntity.user, includeResumes = false) else null,
author = User.fromEntity(resumeEntity.user),
content = resumeEntity.content ?: "",
createdAt = resumeEntity.createdAt,
phoneNumber = resumeEntity.phoneNumber ?: "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.waffletoy.team1server.user

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthUserOrNull
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.waffletoy.team1server.user

import com.waffletoy.team1server.exceptions.ApiException
import com.waffletoy.team1server.exceptions.BadAuthorizationHeaderException
import com.waffletoy.team1server.exceptions.InvalidAccessTokenException
import com.waffletoy.team1server.user.dtos.User
import com.waffletoy.team1server.user.service.UserService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.core.MethodParameter
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer

@Component
class UserOrNullArgumentResolver(
private val userService: UserService,
) : HandlerMethodArgumentResolver {
private val logger: Logger = LoggerFactory.getLogger(UserOrNullArgumentResolver::class.java)

override fun supportsParameter(parameter: MethodParameter): Boolean {
val isSupported =
parameter.parameterType == User::class.java &&
parameter.hasParameterAnnotation(AuthUserOrNull::class.java)
logger.debug("supportsParameter called for parameter '${parameter.parameterName}': $isSupported")
return isSupported
}

// Authorization header 가 없으면 exception 대신 null 을 반환
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?,
): User? {
logger.debug("resolveArgument called for parameter: ${parameter.parameterName}")

val authorizationHeader = webRequest.getHeader("Authorization")
logger.debug("Authorization Header: $authorizationHeader")

// Check if the Authorization header is present and not blank
if (authorizationHeader.isNullOrBlank()) {
logger.warn("Authorization header is missing or blank")
return null
}

// Split the header and validate the format (e.g., "Bearer <token>")
val tokenParts = authorizationHeader.split(" ")
if (tokenParts.size != 2 || tokenParts[0] != "Bearer") {
logger.warn("Authorization header is malformed: $authorizationHeader")
throw BadAuthorizationHeaderException(
details = mapOf("Authorization" to "Malformed Authorization header. Expected format: Bearer <token>"),
)
}

val accessToken = tokenParts[1]
logger.debug("Extracted Access Token: $accessToken")

return try {
// Authenticate the user using the extracted access token
userService.authenticate(accessToken).also {
logger.debug("Authenticated User: $it")
}
} catch (ex: ApiException) {
// Propagate ApiException without modification
logger.error("Authentication failed: ${ex.message}", ex)
throw ex
} catch (ex: Exception) {
// Handle unexpected exceptions by wrapping them in an ApiException
logger.error("Unexpected error during authentication: ${ex.message}", ex)
throw InvalidAccessTokenException(
details = mapOf("error" to ex.message.orEmpty()),
)
}
}
}
7 changes: 3 additions & 4 deletions src/main/kotlin/com/waffletoy/team1server/user/dtos/User.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.waffletoy.team1server.user.dtos

import com.waffletoy.team1server.resume.controller.Resume
import com.waffletoy.team1server.user.UserRole
import com.waffletoy.team1server.user.persistence.UserEntity
import java.time.LocalDateTime
Expand All @@ -13,7 +12,7 @@ data class User(
val userRole: UserRole,
val snuMail: String?,
val phoneNumber: String?,
val resumes: List<Resume>?,
// val resumes: List<Resume>?,
// val posts: List<Post>?,
val profileImageLink: String?,
val isMerged: Boolean,
Expand All @@ -22,7 +21,7 @@ data class User(
fun fromEntity(
entity: UserEntity,
isMerged: Boolean = false,
includeResumes: Boolean = false,
// includeResumes: Boolean = false,
): User =
User(
id = entity.id,
Expand All @@ -32,7 +31,7 @@ data class User(
userRole = entity.userRole,
snuMail = entity.snuMail,
phoneNumber = entity.phoneNumber,
resumes = if (includeResumes) entity.resumes.map { Resume.fromEntity(it, false) } else null,
// resumes = if (includeResumes) entity.resumes.map { Resume.fromEntity(it, false) } else null,
// posts = entity.posts.map { Post.fromEntity(it.roles) },
profileImageLink = entity.profileImageLink,
isMerged = isMerged,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.waffletoy.team1server.user.persistence

import com.waffletoy.team1server.post.persistence.CompanyEntity
import com.waffletoy.team1server.resume.persistence.ResumeEntity
import com.waffletoy.team1server.user.UserRole
import jakarta.persistence.*
import jakarta.validation.ValidationException
Expand Down Expand Up @@ -44,11 +42,13 @@ class UserEntity(
val snuMail: String?,
@Column(name = "phone_number", nullable = true)
var phoneNumber: String? = null,
@OneToMany(mappedBy = "user")
val resumes: MutableList<ResumeEntity> = mutableListOf(),
// Resume Repository 에서 조회하기 때문에 필요 없음
// @OneToMany(mappedBy = "user")
// val resumes: MutableList<ResumeEntity> = mutableListOf(),
// POST-ADMIN specific field
@OneToMany(mappedBy = "admin")
var posts: MutableList<CompanyEntity> = mutableListOf(),
// 역시 Position Entity에서 조회할 예정
// @OneToMany(mappedBy = "admin")
// var posts: MutableList<CompanyEntity> = mutableListOf(),
@Column(name = "profile_image_link", nullable = true)
val profileImageLink: String? = null,
) {
Expand Down

0 comments on commit 3615fc9

Please sign in to comment.