From eab7dc4521c8ed4ba2c8f12081608d969915b2c1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 10 Nov 2025 12:14:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(home-free-content):=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20getLatestContentByTheme=EC=97=90=20orderby?= =?UTF-8?q?Random=20flag=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC?= =?UTF-8?q?=20=EB=9E=9C=EB=8D=A4=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EB=A0=AC?= =?UTF-8?q?=ED=95=9C=20=ED=9B=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=AC=20=EC=88=98=20=EC=9E=88=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/vividnext/sodalive/api/home/HomeService.kt | 3 ++- .../sodalive/content/AudioContentRepository.kt | 14 +++++++++++--- .../sodalive/content/AudioContentService.kt | 6 ++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt index 5d66fe5..096ac98 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt @@ -165,7 +165,8 @@ class HomeService( ), contentType = contentType, isFree = true, - isAdult = isAdult + isAdult = isAdult, + orderByRandom = true ).filter { if (memberId != null) { !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 66abb79..64d294e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -183,7 +183,8 @@ interface AudioContentQueryRepository { offset: Long, limit: Long, isFree: Boolean, - isAdult: Boolean + isAdult: Boolean, + orderByRandom: Boolean = false ): List fun findContentByCurationId( @@ -1308,7 +1309,8 @@ class AudioContentQueryRepositoryImpl( offset: Long, limit: Long, isFree: Boolean, - isAdult: Boolean + isAdult: Boolean, + orderByRandom: Boolean ): List { var where = audioContent.isActive.isTrue .and(audioContent.duration.isNotNull) @@ -1343,6 +1345,12 @@ class AudioContentQueryRepositoryImpl( where = where.and(audioContent.price.loe(0)) } + val orderBy = if (orderByRandom) { + Expressions.numberTemplate(Double::class.java, "function('rand')").asc() + } else { + audioContent.releaseDate.desc() + } + return queryFactory .select( QAudioContentMainItem( @@ -1360,7 +1368,7 @@ class AudioContentQueryRepositoryImpl( .where(where) .offset(offset) .limit(limit) - .orderBy(audioContent.releaseDate.desc()) + .orderBy(orderBy) .fetch() } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 3a9e3a3..a5c4c79 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -989,7 +989,8 @@ class AudioContentService( offset: Long = 0, limit: Long = 20, isFree: Boolean = false, - isAdult: Boolean = false + isAdult: Boolean = false, + orderByRandom: Boolean = false ): List { return repository.getLatestContentByTheme( theme = theme, @@ -997,7 +998,8 @@ class AudioContentService( offset = offset, limit = limit, isFree = isFree, - isAdult = isAdult + isAdult = isAdult, + orderByRandom = orderByRandom ) } } -- 2.49.1 From e24e8372a847b4482f1631787bd8a5727882f20b Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 10 Nov 2025 13:58:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(home):=20=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/api/home/GetHomeResponse.kt | 1 + .../vividnext/sodalive/api/home/HomeService.kt | 17 +++++++++++++++++ .../sodalive/content/AudioContentRepository.kt | 10 ++++++++-- .../sodalive/content/AudioContentService.kt | 6 ++++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt index aa11948..efbd497 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/GetHomeResponse.kt @@ -26,5 +26,6 @@ data class GetHomeResponse( val contentRanking: List, val recommendChannelList: List, val freeContentList: List, + val pointAvailableContentList: List, val curationList: List ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt index 096ac98..b45db6e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt @@ -175,6 +175,22 @@ class HomeService( } } + // 포인트 사용가능 콘텐츠 리스트 - 랜덤으로 가져오기 (DB에서 isPointAvailable 조건 적용) + val pointAvailableContentList = contentService.getLatestContentByTheme( + theme = emptyList(), + contentType = contentType, + isFree = false, + isAdult = isAdult, + orderByRandom = true, + isPointAvailableOnly = true + ).filter { + if (memberId != null) { + !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) + } else { + true + } + } + val curationList = curationService.getContentCurationList( tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용 isAdult = isAdult, @@ -196,6 +212,7 @@ class HomeService( contentRanking = contentRanking, recommendChannelList = recommendChannelList, freeContentList = freeContentList, + pointAvailableContentList = pointAvailableContentList, curationList = curationList ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 64d294e..6b93c4d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -184,7 +184,8 @@ interface AudioContentQueryRepository { limit: Long, isFree: Boolean, isAdult: Boolean, - orderByRandom: Boolean = false + orderByRandom: Boolean = false, + isPointAvailableOnly: Boolean = false ): List fun findContentByCurationId( @@ -1310,7 +1311,8 @@ class AudioContentQueryRepositoryImpl( limit: Long, isFree: Boolean, isAdult: Boolean, - orderByRandom: Boolean + orderByRandom: Boolean, + isPointAvailableOnly: Boolean ): List { var where = audioContent.isActive.isTrue .and(audioContent.duration.isNotNull) @@ -1345,6 +1347,10 @@ class AudioContentQueryRepositoryImpl( where = where.and(audioContent.price.loe(0)) } + if (isPointAvailableOnly) { + where = where.and(audioContent.isPointAvailable.isTrue) + } + val orderBy = if (orderByRandom) { Expressions.numberTemplate(Double::class.java, "function('rand')").asc() } else { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index a5c4c79..23bca75 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -990,7 +990,8 @@ class AudioContentService( limit: Long = 20, isFree: Boolean = false, isAdult: Boolean = false, - orderByRandom: Boolean = false + orderByRandom: Boolean = false, + isPointAvailableOnly: Boolean = false ): List { return repository.getLatestContentByTheme( theme = theme, @@ -999,7 +1000,8 @@ class AudioContentService( limit = limit, isFree = isFree, isAdult = isAdult, - orderByRandom = orderByRandom + orderByRandom = orderByRandom, + isPointAvailableOnly = isPointAvailableOnly ) } } -- 2.49.1 From 82bd93c1ae3006b828994f1b7bb29184e0e76b49 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 10 Nov 2025 14:39:44 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(admin-member):=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EA=B2=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20id,=20nickname=20=EB=B0=98=ED=99=98=20API=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 --- .../admin/member/AdminMemberController.kt | 6 ++++++ .../admin/member/AdminMemberRepository.kt | 19 +++++++++++++++++++ .../admin/member/AdminMemberService.kt | 6 ++++++ .../admin/member/AdminSimpleMemberResponse.kt | 12 ++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminSimpleMemberResponse.kt 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 index 7070169..ca129a2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -36,6 +36,12 @@ class AdminMemberController(private val service: AdminMemberService) { pageable: Pageable ) = ApiResponse.ok(service.searchMember(searchWord, pageable)) + @GetMapping("/search-by-nickname") + fun searchMemberByNickname( + @RequestParam(value = "search_word") searchWord: String, + @RequestParam(value = "size", required = false) size: Int? + ) = ApiResponse.ok(service.searchMemberByNickname(searchWord = searchWord, size = size ?: 20)) + @GetMapping("/creator/all/list") fun getCreatorAllList() = ApiResponse.ok(service.getCreatorAllList()) 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 index 538259b..4249bd3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -16,6 +16,7 @@ interface AdminMemberQueryRepository { fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int fun getCreatorAllList(): List fun findByIdAndActive(memberId: Long): Member? + fun searchMemberByNickname(searchWord: String, limit: Long = 20): List } class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository { @@ -121,4 +122,22 @@ class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .orderBy(member.id.desc()) .fetchFirst() } + + override fun searchMemberByNickname(searchWord: String, limit: Long): List { + return queryFactory + .select( + QAdminSimpleMemberResponse( + member.id, + member.nickname + ) + ) + .from(member) + .where( + member.nickname.contains(searchWord) + .and(member.isActive.isTrue) + ) + .orderBy(member.id.desc()) + .limit(limit) + .fetch() + } } 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 index 3946cbe..5a1f50f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -145,6 +145,12 @@ class AdminMemberService( return repository.getCreatorAllList() } + fun searchMemberByNickname(searchWord: String, size: Int = 20): List { + if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") + val limit = if (size <= 0) 20 else size + return repository.searchMemberByNickname(searchWord = searchWord, limit = limit.toLong()) + } + @Transactional fun resetPassword(request: ResetPasswordRequest) { val member = repository.findByIdAndActive(memberId = request.memberId) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminSimpleMemberResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminSimpleMemberResponse.kt new file mode 100644 index 0000000..00d0ea5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminSimpleMemberResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.member + +import com.querydsl.core.annotations.QueryProjection + +/** + * 관리자용 간단 회원 응답 DTO + * 닉네임 검색 결과로 사용되며 charge 등에서 memberId 선택에 활용된다. + */ +data class AdminSimpleMemberResponse @QueryProjection constructor( + val id: Long, + val nickname: String +) -- 2.49.1 From 26c09de7c9c2e03665de9398ffa10f12a8cc9aab Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 10 Nov 2025 15:15:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(admin-can):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=BA=94=20=EC=B6=A9=EC=A0=84=20API=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=A4=EC=A4=91=20=ED=9A=8C=EC=9B=90=20=EC=9D=BC=EA=B4=84=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=EC=9C=BC=EB=A1=9C=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdminCanChargeRequest: memberId → memberIds(List)로 변경 - AdminCanService.charge: memberIds 선조회 후 다건 충전 로직 추가 - 잘못된/비어있는 회원번호 검증 및 트랜잭션 롤백으로 정합성 보장 배경: 관리자 일괄 충전 요구사항 반영으로 여러 회원에게 동일 수량의 캔을 한 번에 충전할 수 있도록 개선. 중복 ID는 제거하여 중복 충전을 방지하고, 하나라도 유효하지 않으면 전체 롤백되도록 처리하여 데이터 정합성 확보. --- .../admin/can/AdminCanChargeRequest.kt | 2 +- .../sodalive/admin/can/AdminCanService.kt | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt index bbb8fd0..22cfb98 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.admin.can data class AdminCanChargeRequest( - val memberId: Long, + val memberIds: List, val method: String, val can: Int ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt index 612e414..9e780dc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt @@ -40,22 +40,27 @@ class AdminCanService( @Transactional fun charge(request: AdminCanChargeRequest) { - val member = memberRepository.findByIdOrNull(request.memberId) - ?: throw SodaException("잘못된 회원번호 입니다.") - if (request.can <= 0) throw SodaException("1 캔 이상 입력하세요.") if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.") - val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) - charge.title = "${request.can.moneyFormat()} 캔" - charge.member = member + val ids = request.memberIds.distinct() + if (ids.isEmpty()) throw SodaException("회원번호를 입력하세요.") - val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG) - payment.method = request.method - charge.payment = payment + val members = memberRepository.findAllById(ids).toList() + if (members.size != ids.size) throw SodaException("잘못된 회원번호 입니다.") - chargeRepository.save(charge) + members.forEach { member -> + val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) + charge.title = "${request.can.moneyFormat()} 캔" + charge.member = member - member.pgRewardCan += charge.rewardCan + val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG) + payment.method = request.method + charge.payment = payment + + chargeRepository.save(charge) + + member.pgRewardCan += charge.rewardCan + } } } -- 2.49.1