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: 특정 플레이어의 퀴즈 목록 조회 #65

Merged
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
5 changes: 5 additions & 0 deletions src/main/kotlin/yjh/cstar/common/BaseErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ enum class BaseErrorCode(
),
PASSWORD_INVALID(HttpStatus.BAD_REQUEST, 40012, "비밀번호가 일치하지 않습니다."),
QUIZ_CATEGORY_INVALID(HttpStatus.BAD_REQUEST, 40020, "지원하지 않는 퀴즈 카테고리 입니다."),
QUIZ_FILTER_INVALID(
HttpStatus.BAD_REQUEST,
40021,
"유효하지 않은 퀴즈 필터입니다. 사용 가능한 필터: created(생성한 문제), attempted(시도한 문제), correct(정답을 맞춘 문제)."
),

// 401
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 4010, "인증이 필요합니다."),
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/yjh/cstar/quiz/application/QuizService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import yjh.cstar.quiz.application.port.QuizFilter.QuizFilterFactory
import yjh.cstar.quiz.application.port.QuizRepository
import yjh.cstar.quiz.domain.Category
import yjh.cstar.quiz.domain.Quiz
Expand All @@ -13,6 +14,7 @@ import yjh.cstar.quiz.domain.QuizCreateCommand
@Service
class QuizService(
val quizRepository: QuizRepository,
val quizFilterFactory: QuizFilterFactory,
) {
fun getQuizzes(quizCategory: String, totalQuestions: Int): List<Quiz> {
val category = Category.create(quizCategory)
Expand All @@ -29,4 +31,9 @@ class QuizService(
val category = Category.create(quizCategory)
return quizRepository.findAllByCategory(category, pageable)
}

fun retrieveAllByQuizFilterType(memberId: Long, quizFilter: String, pageable: Pageable): Page<Quiz> {
val quizFilterType = quizFilterFactory.filterType(quizFilter)
return quizFilterType.filter(memberId, pageable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package yjh.cstar.quiz.application.port.QuizFilter

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import yjh.cstar.quiz.application.port.QuizRepository
import yjh.cstar.quiz.domain.Quiz

class AttemptedQuizFilter(
private val quizRepository: QuizRepository,
) : QuizFilter {
override fun filter(memberId: Long, pageable: Pageable): Page<Quiz> {
return quizRepository.findAllAttemptedByMember(memberId, pageable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package yjh.cstar.quiz.application.port.QuizFilter

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import yjh.cstar.quiz.application.port.QuizRepository
import yjh.cstar.quiz.domain.Quiz

class CreatedQuizFilter(
private val quizRepository: QuizRepository,
) : QuizFilter {
override fun filter(writerId: Long, pageable: Pageable): Page<Quiz> {
return quizRepository.findAllCreatedByMember(writerId, pageable)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package yjh.cstar.quiz.application.port.QuizFilter

import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import yjh.cstar.quiz.domain.Quiz

interface QuizFilter {
fun filter(memberId: Long, pageable: Pageable): Page<Quiz>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package yjh.cstar.quiz.application.port.QuizFilter

import org.springframework.stereotype.Component
import yjh.cstar.common.BaseErrorCode
import yjh.cstar.common.BaseException
import yjh.cstar.quiz.application.port.QuizRepository
import yjh.cstar.quiz.domain.QuizFilterType

@Component
class QuizFilterFactory(
private val quizRepository: QuizRepository,
) {
fun filterType(quizFilterType: String): QuizFilter {
return when (QuizFilterType.create(quizFilterType)) {
QuizFilterType.CREATED -> CreatedQuizFilter(quizRepository)
QuizFilterType.ATTEMPTED -> AttemptedQuizFilter(quizRepository)
else -> throw BaseException(BaseErrorCode.QUIZ_FILTER_INVALID)
}
Comment on lines +14 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

코틀린 when 문법으로 깔끔하게 작성하신게 인상깊네요!!
나중에 여러 종류의 필터타입등이 많이 추가될 수 도 있을 것 같은데, 이런 동적으로 받을 수 있는 쿼리스프링 타입에 대해서 어떻게 조회해야될지 고민해보면 좋을 것 같습니다!!

Copy link
Member Author

Choose a reason for hiding this comment

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

네! 다음 조회 기능을 구현할 때 같이 논의해보면 좋을 것 같습니다!😆

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface QuizRepository {
fun save(quiz: Quiz): Quiz
fun getQuizzes(quizCategory: String, totalQuestions: Int): List<Quiz>
fun findAllByCategory(category: Category, pageable: Pageable): Page<Quiz>
fun findAllCreatedByMember(writerId: Long, pageable: Pageable): Page<Quiz>
fun findAllAttemptedByMember(memberId: Long, pageable: Pageable): Page<Quiz>
}
17 changes: 17 additions & 0 deletions src/main/kotlin/yjh/cstar/quiz/domain/QuizFilterType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package yjh.cstar.quiz.domain

import yjh.cstar.common.BaseErrorCode
import yjh.cstar.common.BaseException

enum class QuizFilterType(val description: String) {
CREATED("created"),
ATTEMPTED("attempted"),
CORRECT("correct"),
;

companion object {
fun create(quizFilter: String): QuizFilterType =
entries.firstOrNull { it.description == quizFilter }
?: throw BaseException(BaseErrorCode.QUIZ_FILTER_INVALID)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ class QuizRepositoryAdapter(
override fun findAllByCategory(category: Category, pageable: Pageable): Page<Quiz> {
return quizJpaRepository.findAllByCategory(category, pageable).map { it.toModel() }
}

override fun findAllCreatedByMember(writerId: Long, pageable: Pageable): Page<Quiz> {
return quizJpaRepository.findAllCreatedByMember(writerId, pageable).map { it.toModel() }
}

override fun findAllAttemptedByMember(memberId: Long, pageable: Pageable): Page<Quiz> {
return quizJpaRepository.findAllAttemptedByMember(memberId, pageable).map { it.toModel() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,27 @@ interface QuizJpaRepository : JpaRepository<QuizEntity, Long> {

@Query("SELECT q FROM QuizEntity q WHERE q.deletedAt IS NULL AND q.category = :category")
fun findAllByCategory(@Param("category") category: Category, pageable: Pageable): Page<QuizEntity>

@Query("SELECT q FROM QuizEntity q WHERE q.deletedAt IS NULL AND q.writerId = :memberId")
fun findAllCreatedByMember(@Param("memberId") writerId: Long, pageable: Pageable): Page<QuizEntity>
Comment on lines +30 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

@query문을 아래 처럼 보기좋게 정렬하면 좋을 것 같아요!!

Copy link
Member Author

Choose a reason for hiding this comment

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

네 보기좋게 정렬해보겠습니다!😀


@Query(
value = """
SELECT q.*
FROM quiz q
WHERE q.deleted_at IS NULL
AND q.quiz_id IN (
SELECT gq.quiz_id
FROM game_quiz gq
JOIN member_game_result mgr ON mgr.game_id = gq.game_id
WHERE mgr.member_id = :memberId
AND mgr.total_count > 0
Comment on lines +35 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 쿼리는 여태까지 시도해봤던 모든 퀴즈 기록들을 조회하는 것인가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

네 맞습니다. 특정 멤버가 시도한 퀴즈 전체를 조회합니다. 조회 쿼리 같은 경우 어떤 테이블을 조합해서 어떤 데이터를 구체적으로 가져오는지 앞으로는 더 명확하게 적도록 하겠습니다. 😂

)
""",
nativeQuery = true
)
fun findAllAttemptedByMember(
@Param("memberId") memberId: Long,
pageable: Pageable,
): Page<QuizEntity>
}
12 changes: 12 additions & 0 deletions src/main/kotlin/yjh/cstar/quiz/presentation/QuizController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,16 @@ class QuizController(
val responses = quizService.retrieveAllByCategory(category, pageable).map { QuizResponse.from(it) }
return ResponseEntity.ok(Response(data = responses))
}

@GetMapping("/quizzes/filter")
fun retrieveAllByQuizFilterType(
@RequestParam quizFilterType: String,
@PageableDefault(size = 10) pageable: Pageable,
authentication: Authentication,
): ResponseEntity<Response<Page<QuizResponse>>> {
val memberId = tokenProvider.getMemberId(authentication)
val responses = quizService.retrieveAllByQuizFilterType(memberId, quizFilterType, pageable)
.map { QuizResponse.from(it) }
return ResponseEntity.ok(Response(data = responses))
}
}
183 changes: 182 additions & 1 deletion src/test/kotlin/yjh/cstar/quiz/application/QuizServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import yjh.cstar.IntegrationTest
import yjh.cstar.common.BaseException
import yjh.cstar.game.infrastructure.jpa.GameEntity
import yjh.cstar.game.infrastructure.jpa.GameJpaRepository
import yjh.cstar.game.infrastructure.jpa.GameResultEntity
import yjh.cstar.game.infrastructure.jpa.GameResultJpaRepository
import yjh.cstar.quiz.domain.Category
import yjh.cstar.quiz.domain.QuizCreateCommand
import yjh.cstar.quiz.infrastructure.jpa.GameQuizEntity
import yjh.cstar.quiz.infrastructure.jpa.GameQuizId
import yjh.cstar.quiz.infrastructure.jpa.GameQuizJpaRepository
import yjh.cstar.quiz.infrastructure.jpa.QuizEntity
import yjh.cstar.quiz.infrastructure.jpa.QuizJpaRepository
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
Expand All @@ -27,6 +35,15 @@ class QuizServiceTest : IntegrationTest() {
@Autowired
private lateinit var quizJpaRepository: QuizJpaRepository

@Autowired
private lateinit var gameJpaRepository: GameJpaRepository

@Autowired
private lateinit var gameResultJpaRepository: GameResultJpaRepository

@Autowired
private lateinit var gameQuizJpaRepository: GameQuizJpaRepository

@Test
fun `퀴즈 생성 테스트`() {
// given
Expand Down Expand Up @@ -172,7 +189,7 @@ class QuizServiceTest : IntegrationTest() {
}

@Test
fun `퀴즈 카테고리별로 퀴즈를 조회 할 때, 잘못된 카테고리 이름 입력 시 BAD_REQUEST 에러 발생 테스트 `() {
fun `퀴즈 카테고리별로 퀴즈를 조회 할 때, 잘못된 카테고리 이름 입력 시 BAD_REQUEST 에러 발생 테스트`() {
// given
val quizCategory = "운영체제임"

Expand All @@ -187,4 +204,168 @@ class QuizServiceTest : IntegrationTest() {
// then
assertEquals(HttpStatus.BAD_REQUEST, exception.baseErrorCode.httpStatus)
}

@Test
fun `특정 플레이어가 생성한 퀴즈를 조회하는 테스트`() {
// given
val memberId = 1L
val quizFilter = "created"

quizJpaRepository.save(
QuizEntity(
writerId = 1L,
question = "문제11",
answer = "정답11",
category = Category.NETWORK,
createdAt = null,
updatedAt = null
)
)
quizJpaRepository.save(
QuizEntity(
writerId = 1L,
question = "문제22",
answer = "정답22",
category = Category.DATABASE,
createdAt = null,
updatedAt = null
)
)
quizJpaRepository.save(
QuizEntity(
writerId = 2L,
question = "문제33",
answer = "정답33",
category = Category.ALGORITHM,
createdAt = null,
updatedAt = null
)
)

// when
val pageByCreated = quizService.retrieveAllByQuizFilterType(
memberId,
quizFilter,
pageable = PageRequest.of(0, 10)
)

// then
assertEquals(2, pageByCreated.content.size)
}

@Test
fun `특정 플레이어가 생성한 퀴즈를 조회할 때 지원하지 않는 퀴즈 필터 입력 시 BAD_REQUEST 에러 발생 테스트`() {
// given
val memberId = 2L
val quizFilter = "corrected"

quizJpaRepository.save(
QuizEntity(
writerId = 2L,
question = "문제33",
answer = "정답33",
category = Category.ALGORITHM,
createdAt = null,
updatedAt = null
)
)

// when
val exception = assertThrows<BaseException> {
quizService.retrieveAllByQuizFilterType(
memberId,
quizFilter,
pageable = PageRequest.of(0, 10)
)
}

// then
assertEquals(HttpStatus.BAD_REQUEST, exception.baseErrorCode.httpStatus)
}

@Test
fun `특정 플레이어가 시도한 퀴즈(푼 문제 + 못 푼 문제)를 조회하는 테스트`() {
// given
val memberId = 3L
val quizFilter = "attempted"

// 퀴즈 생성
val quiz1 = quizJpaRepository.save(
QuizEntity(
id = 1L,
writerId = 1L,
question = "문제111",
answer = "정답111",
category = Category.ALGORITHM,
createdAt = null,
updatedAt = null
)
).toModel()

val quiz2 = quizJpaRepository.save(
QuizEntity(
id = 2L,
writerId = 1L,
question = "문제222",
answer = "정답222",
category = Category.ALGORITHM,
createdAt = null,
updatedAt = null
)
).toModel()

val quiz3 = quizJpaRepository.save(
QuizEntity(
id = 3L,
writerId = 1L,
question = "문제333",
answer = "정답333",
category = Category.ALGORITHM,
createdAt = null,
updatedAt = null
)
).toModel()

// 게임에 참가
val savedGame = gameJpaRepository.save(
GameEntity(
roomId = 1L,
winnerId = 2L,
totalQuizCount = 3,
startedAt = LocalDateTime.now(),
createdAt = null,
updatedAt = null
)
).toModel()

gameResultJpaRepository.save(
GameResultEntity(
gameId = savedGame.id,
playerId = memberId,
totalCount = 3,
correctCount = 2,
ranking = 1,
createdAt = null,
updatedAt = null
)
).toModel().id

gameQuizJpaRepository.saveAll(
listOf(
GameQuizEntity(id = GameQuizId(gameId = savedGame.id, quizId = quiz1.id)),
GameQuizEntity(id = GameQuizId(gameId = savedGame.id, quizId = quiz2.id)),
GameQuizEntity(id = GameQuizId(gameId = savedGame.id, quizId = quiz3.id))
)
)

// when
val pageByCreated = quizService.retrieveAllByQuizFilterType(
memberId,
quizFilter,
pageable = PageRequest.of(0, 10)
)

// then
assertEquals(3, pageByCreated.content.size)
}
}