diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt new file mode 100644 index 0000000..78389da --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.admin.member + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin/member") +class AdminMemberController(private val service: AdminMemberService) { + @GetMapping("/list") + @PreAuthorize("hasRole('ADMIN')") + fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable)) + + @GetMapping("/search") + fun searchMember( + @RequestParam(value = "search_word") searchWord: String, + pageable: Pageable + ) = ApiResponse.ok(service.searchMember(searchWord, pageable)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt new file mode 100644 index 0000000..9d20fe9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -0,0 +1,95 @@ +package kr.co.vividnext.sodalive.admin.member + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository + +interface AdminMemberRepository : JpaRepository, AdminMemberQueryRepository + +interface AdminMemberQueryRepository { + fun getMemberTotalCount(role: MemberRole? = null): Int + fun getMemberList(offset: Long, limit: Long, role: MemberRole? = null): List + fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List + + fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int +} + +class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository { + override fun getMemberList(offset: Long, limit: Long, role: MemberRole?): List { + return queryFactory + .selectFrom(member) + .where( + member.role.ne(MemberRole.ADMIN) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .offset(offset) + .limit(limit) + .orderBy(member.id.desc()) + .fetch() + } + + override fun getMemberTotalCount(role: MemberRole?): Int { + return queryFactory + .select(member.id) + .from(member) + .where( + member.id.gt(1) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .fetch() + .size + } + + override fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole?): List { + return queryFactory + .selectFrom(member) + .where( + member.nickname.contains(searchWord) + .or(member.email.contains(searchWord)) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .offset(offset) + .limit(limit) + .orderBy(member.id.desc()) + .fetch() + } + + override fun searchMemberTotalCount(searchWord: String, role: MemberRole?): Int { + return queryFactory + .select(member.id) + .from(member) + .where( + member.nickname.contains(searchWord) + .or(member.email.contains(searchWord)) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt new file mode 100644 index 0000000..49f1d46 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -0,0 +1,92 @@ +package kr.co.vividnext.sodalive.admin.member + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AdminMemberService( + private val repository: AdminMemberRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getMemberList(pageable: Pageable): GetAdminMemberListResponse { + val totalCount = repository.getMemberTotalCount() + val memberList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.getMemberList( + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + + return GetAdminMemberListResponse(totalCount, memberList) + } + + fun searchMember(searchWord: String, pageable: Pageable): GetAdminMemberListResponse { + if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") + val totalCount = repository.searchMemberTotalCount(searchWord = searchWord) + val memberList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.searchMember( + searchWord = searchWord, + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + + return GetAdminMemberListResponse(totalCount, memberList) + } + + private fun processMemberListToGetAdminMemberListResponseItemList( + memberList: List + ): List { + return memberList + .asSequence() + .map { + val userType = when (it.role) { + MemberRole.ADMIN -> "관리자" + MemberRole.USER -> "일반회원" + MemberRole.CREATOR -> "요즘친구" + MemberRole.AGENT -> "에이전트" + MemberRole.BOT -> "봇" + } + + val signUpDate = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + + val signOutDate = if (it.signOutReasons.isNotEmpty()) { + it.signOutReasons.last().createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + } else { + "" + } + + GetAdminMemberListResponseItem( + id = it.id!!, + email = it.email, + nickname = it.nickname, + profileUrl = if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + userType = userType, + container = it.container, + auth = it.auth != null, + signUpDate = signUpDate, + signOutDate = signOutDate, + isActive = it.isActive + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt new file mode 100644 index 0000000..1fd1c47 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.admin.member + +data class GetAdminMemberListResponse( + val totalCount: Int, + val items: List +) + +data class GetAdminMemberListResponseItem( + val id: Long, + val email: String, + val nickname: String, + val profileUrl: String, + val userType: String, + val container: String, + val auth: Boolean, + val signUpDate: String, + val signOutDate: String, + val isActive: Boolean +)