Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 8 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
build:
name: Deploy API specification
runs-on: ubuntu-latest
if: github.ref_name == 'dev'
environment: ${{ github.ref_name }}
steps:
- name: Checkout repository
Expand Down Expand Up @@ -95,7 +96,7 @@ jobs:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: docker/docker-compose.yml
source: docker/docker-compose-${{ github.ref_name }}.yml
target: "~"
strip_components: 1
- name: Build and deploy container to AWS EC2
Expand All @@ -106,9 +107,12 @@ jobs:
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
aws secretsmanager get-secret-value --secret-id ${{ github.ref_name }}-env --region ap-northeast-2 --query SecretString --output text | jq -r '. | to_entries | map("\(.key)=\(.value)") | .[]' > .env
aws s3 cp s3://${{ secrets.API_SPECIFICATION_BUCKET }}/api.yml .
export IMAGE_URI=${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}:${{ github.sha }}
docker-compose --env-file .env up -d --build
docker-compose -f docker-compose-${{ github.ref_name }}.yml --env-file .env up -d --build
if [ "${{ github.ref_name }}" = "dev" ]; then
aws s3 cp s3://${{ secrets.API_SPECIFICATION_BUCKET }}/api.yml .
docker restart swagger
fi
notify-discord:
name: Notify Discord
runs-on: ubuntu-latest
Expand All @@ -127,7 +131,7 @@ jobs:
"embeds": [
{
"title": "**${{ github.ref_name == 'prod' && '프로덕션' || '개발' }} 환경 배포 성공**",
"description": "프로젝트가 성공적으로 배포되었습니다.",
"description": "서버가 성공적으로 배포되었습니다.",
"color": 3066993,
"fields": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,14 @@ class AdminController(
) {
adminCommandUseCase.deleteExamHistoryByExamIdAndUserId(examId, userId)
}

@DeleteMapping("/admin/users/{userId}/badges/{badgeId}")
fun deleteUserBadge(
@PathVariable
userId: Long,
@PathVariable
badgeId: Long
) {
adminCommandUseCase.deleteUserBadgeByBadgeIdAndUserId(userId, badgeId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.gotchai.api.presentation.v1.admin

import com.gotchai.api.common.ControllerTest
import com.gotchai.api.docs.createExamRequestFields
import com.gotchai.api.docs.errorResponseFields
import com.gotchai.api.docs.examResponseFields
import com.gotchai.api.fixture.createCreateExamRequest
import com.gotchai.api.global.dto.ApiResponse
Expand All @@ -11,6 +12,7 @@ import com.gotchai.api.util.desc
import com.gotchai.api.util.document
import com.gotchai.api.util.expectError
import com.gotchai.domain.admin.port.`in`.AdminCommandUseCase
import com.gotchai.domain.badge.exception.UserBadgeNotFoundException
import com.gotchai.domain.exam.exception.ExamHistoryNotFoundException
import com.gotchai.domain.fixture.ID
import com.gotchai.domain.fixture.createExam
Expand Down Expand Up @@ -87,6 +89,50 @@ class AdminControllerTest : ControllerTest() {
"userId" desc "유저 식별자",
"examId" desc "테스트 식별자"
)
responseBody(errorResponseFields)
}
}
}
}

describe("deleteUserBadge()는") {
context("유저가 취득한 뱃지가 존재하는 경우") {
every { adminCommandUseCase.deleteUserBadgeByBadgeIdAndUserId(ID, ID) } just runs

it("상태 코드 200을 반환한다.") {
webClient
.delete()
.uri("/api/v1/admin/users/{userId}/badges/{badgeId}", ID, ID)
.exchange()
.expectStatus()
.isOk
.expectBody<Void>()
.document("유저가 취득한 뱃지 삭제 성공(200)") {
pathParams(
"userId" desc "유저 식별자",
"badgeId" desc "뱃지 식별자"
)
}
}
}

context("유저가 취득한 뱃지가 존재하지 않는 경우") {
every { adminCommandUseCase.deleteUserBadgeByBadgeIdAndUserId(ID, ID) } throws UserBadgeNotFoundException()

it("상태 코드 404를 반환한다.") {
webClient
.delete()
.uri("/api/v1/admin/users/{userId}/badges/{badgeId}", ID, ID)
.exchange()
.expectStatus()
.isNotFound
.expectError()
.document("유저가 취득한 뱃지 삭제 실패(404)") {
pathParams(
"userId" desc "유저 식별자",
"badgeId" desc "뱃지 식별자"
)
responseBody(errorResponseFields)
}
}
}
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion docker/docker-compose-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
- "name=redis"
- "mode=standalone"
mysql:
image: mysql:8.4.3
image: mysql
container_name: mysql
restart: always
ports:
Expand Down
21 changes: 21 additions & 0 deletions docker/docker-compose-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
gotchai-server:
image: ${IMAGE_URI}
container_name: gotchai-server
restart: always
environment:
- TZ=Asia/Seoul
ports:
- "8080:8080"
depends_on:
- redis
env_file:
- .env
redis:
image: redis:alpine
container_name: redis
ports:
- "6379:6379"
environment:
- TZ=Asia/Seoul
command: [ "redis-server", "--maxmemory", "64mb" ]
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.gotchai.domain.admin.adapter.`in`
import com.gotchai.domain.admin.dto.command.CreateExamCommand
import com.gotchai.domain.admin.exception.InvalidFileException
import com.gotchai.domain.admin.port.`in`.AdminCommandUseCase
import com.gotchai.domain.badge.exception.UserBadgeNotFoundException
import com.gotchai.domain.badge.port.out.UserBadgeCommandPort
import com.gotchai.domain.badge.port.out.UserBadgeQueryPort
import com.gotchai.domain.exam.entity.Exam
import com.gotchai.domain.exam.exception.ExamHistoryNotFoundException
import com.gotchai.domain.exam.port.out.ExamCommandPort
Expand All @@ -20,6 +23,8 @@ class AdminCommandService(
private val examHistoryQueryPort: ExamHistoryQueryPort,
private val examHistoryCommandPort: ExamHistoryCommandPort,
private val quizHistoryCommandPort: QuizHistoryCommandPort,
private val userBadgeQueryPort: UserBadgeQueryPort,
private val userBadgeCommandPort: UserBadgeCommandPort,
private val objectStorageProvider: ObjectStorageProvider
) : AdminCommandUseCase {
@Transactional
Expand Down Expand Up @@ -55,4 +60,14 @@ class AdminCommandService(
quizHistoryCommandPort.deleteQuizHistoriesByExamHistoryId(examHistory.id)
examHistoryCommandPort.deleteExamHistoryByExamIdAndUserId(examId, userId)
}

@Transactional
override fun deleteUserBadgeByBadgeIdAndUserId(
badgeId: Long,
userId: Long
) {
val userBadge = userBadgeQueryPort.getUserBadgeByBadgeIdAndUserId(badgeId, userId) ?: throw UserBadgeNotFoundException()

userBadgeCommandPort.deleteUserBadgeById(userBadge.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ interface AdminCommandUseCase {
examId: Long,
userId: Long
)

fun deleteUserBadgeByBadgeIdAndUserId(
badgeId: Long,
userId: Long
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.gotchai.domain.badge.exception

import com.gotchai.domain.global.exception.ServerException

class UserBadgeNotFoundException(
override val message: String = "유저가 취득한 뱃지를 찾을 수 없습니다."
) : ServerException(status = 404, message)
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ import com.gotchai.domain.badge.entity.UserBadge

interface UserBadgeCommandPort {
fun createUserBadge(creation: UserBadge.Creation): UserBadge

fun deleteUserBadgeById(id: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ import com.gotchai.domain.badge.entity.UserBadge

interface UserBadgeQueryPort {
fun getUserBadgesByUserId(userId: Long): List<UserBadge>

fun getUserBadgeByBadgeIdAndUserId(
badgeId: Long,
userId: Long
): UserBadge?
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ class UserQueryService(
) : UserQueryUseCase {
override fun getUserRanking(userId: Long): GetUserRankingResult {
val profile = profileQueryPort.getProfileByUserId(userId) ?: throw ProfileNotFoundException()
val allHistories = examHistoryQueryPort.getAllExamHistoriesWithQuizIds()
val allSolvedHistories =
examHistoryQueryPort
.getAllExamHistoriesWithQuizIds()
.filter { it.isSolved }

val rating = calculateUserRating(userId, allHistories)
val rating = calculateUserRating(userId, allSolvedHistories)
return GetUserRankingResult.of(profile, rating)
}

Expand All @@ -38,10 +41,17 @@ class UserQueryService(
}

val userScore = userScores[userId] ?: return 100
if (userScores.size == 1) return 0
if (userScores.size == 1) return 1

val usersWithHigherScore = userScores.values.count { it > userScore }
val rating = (usersWithHigherScore.toDouble() / userScores.size.toDouble()) * 100
return ceil(rating).toInt()

return when {
usersWithHigherScore == 0 && userScores.values.all { it == userScore } -> 50
usersWithHigherScore == 0 -> 1
else -> {
val rating = (usersWithHigherScore.toDouble() / userScores.size.toDouble()) * 100
ceil(rating).toInt().coerceAtLeast(1) // 최소 1% 보장
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ class UserBadgeCommandAdapter(
) : UserBadgeCommandPort {
override fun createUserBadge(creation: UserBadge.Creation): UserBadge =
userBadgeJpaRepository.save(UserBadgeEntity.from(creation)).toUserBadge()

override fun deleteUserBadgeById(id: Long) {
userBadgeJpaRepository.deleteById(id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.gotchai.storage.rdb.badge.adapter.out

import com.gotchai.domain.badge.entity.UserBadge
import com.gotchai.domain.badge.port.out.UserBadgeQueryPort
import com.gotchai.storage.rdb.badge.entity.UserBadgeEntity
import com.gotchai.storage.rdb.badge.repository.UserBadgeJpaRepository
import com.gotchai.storage.rdb.global.annotation.Adapter
import com.gotchai.storage.rdb.global.annotation.ReadOnlyTransactional
Expand All @@ -15,5 +14,14 @@ class UserBadgeQueryAdapter(
override fun getUserBadgesByUserId(userId: Long): List<UserBadge> =
userBadgeRepository
.findAllByUserId(userId)
.map(UserBadgeEntity::toUserBadge)
.map { it.toUserBadge() }

@ReadOnlyTransactional
override fun getUserBadgeByBadgeIdAndUserId(
badgeId: Long,
userId: Long
): UserBadge? =
userBadgeRepository
.findByBadgeIdAndUserId(badgeId, userId)
?.toUserBadge()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@ import com.gotchai.storage.rdb.badge.entity.UserBadgeEntity
import org.springframework.data.jpa.repository.JpaRepository

interface UserBadgeJpaRepository : JpaRepository<UserBadgeEntity, Long> {
fun findByBadgeIdAndUserId(
badgeId: Long,
userId: Long
): UserBadgeEntity?

fun findAllByUserId(userId: Long): List<UserBadgeEntity>
}
Loading