Skip to content

Commit

Permalink
Merge pull request #68 from SUIN-BUNDANG-LINE/SBL-148-survey-result-a…
Browse files Browse the repository at this point in the history
…pi-fix2

[SBL-148] 설문 결과 API를 클라이언트 요구사항에 맞게 수정, 선택지와 필터에 개수 제한 추가
  • Loading branch information
JeongHunHui authored Sep 24, 2024
2 parents 88b89ea + 153a4e6 commit a96ec0a
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum class ErrorCode(
INVALID_FINISHED_AT(HttpStatus.BAD_REQUEST, "SV0022", "유효하지 않은 마감일입니다."),
INVALID_SURVEY_EDIT(HttpStatus.BAD_REQUEST, "SV0023", "설문 수정 상태 변경에 실패했습니다."),
INVALID_PUBLISHED_AT(HttpStatus.BAD_REQUEST, "SV0024", "설문 마감일이 설문 공개일 보다 빠릅니다."),
INVALID_RESULT_FILTER(HttpStatus.BAD_REQUEST, "SV0025", "필터는 최대 20개 까지만 적용 가능합니다."),

// Drawing (DR)
INVALID_DRAWING_BOARD(HttpStatus.BAD_REQUEST, "DR0001", "유효하지 않은 추첨 보드입니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sbl.sulmun2yong.survey.adapter

import com.sbl.sulmun2yong.survey.domain.response.SurveyResponse
import com.sbl.sulmun2yong.survey.domain.result.QuestionResult
import com.sbl.sulmun2yong.survey.domain.result.ResultDetails
import com.sbl.sulmun2yong.survey.domain.result.SurveyResult
import com.sbl.sulmun2yong.survey.entity.ResponseDocument
Expand Down Expand Up @@ -45,15 +46,20 @@ class ResponseAdapter(
responseRepository.findBySurveyId(surveyId)
}
// TODO: 추후 DB Level에서 처리하도록 변경 + 필터링을 동적쿼리로 하도록 변경
val groupingResponses = responses.groupBy { "${it.questionId}|${it.participantId}" }.values
groupingResponses.map { it.toDomain() }
return SurveyResult(resultDetails = groupingResponses.map { it.toDomain() })
val groupingResponses = responses.groupBy { it.questionId }.values
return SurveyResult(questionResults = groupingResponses.map { it.toDomain() })
}

private fun List<ResponseDocument>.toDomain() =
ResultDetails(
QuestionResult(
questionId = first().questionId,
participantId = first().participantId,
contents = map { responseDocument -> responseDocument.content },
resultDetails =
this.groupBy { it.participantId }.map {
ResultDetails(
participantId = it.key,
contents = it.value.map { responseDocument -> responseDocument.content },
)
},
contents = this.map { it.content }.toSortedSet(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ data class Choices(
/** 기타 선택지 허용 여부 */
val isAllowOther: Boolean,
) {
companion object {
const val MAX_SIZE = 20
}

init {
if (standardChoices.isEmpty()) throw InvalidChoiceException()
if (standardChoices.size > MAX_SIZE) throw InvalidChoiceException()
}

/** 응답이 선택지에 포함되는지 확인 */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sbl.sulmun2yong.survey.domain.result

import java.util.SortedSet
import java.util.UUID

data class QuestionResult(
val questionId: UUID,
val resultDetails: List<ResultDetails>,
/** 해당 질문의 모든 응답 집합 */
val contents: SortedSet<String>,
) {
fun getMatchedParticipants(questionFilter: QuestionFilter): Set<UUID> =
resultDetails.mapNotNull { if (it.isMatched(questionFilter)) it.participantId else null }.toSet()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@ import com.sbl.sulmun2yong.survey.exception.InvalidResultDetailsException
import java.util.UUID

data class ResultDetails(
val questionId: UUID,
val participantId: UUID,
val contents: List<String>,
) {
init {
require(contents.isNotEmpty()) { throw InvalidResultDetailsException() }
}

fun isMatched(questionFilter: QuestionFilter): Boolean {
if (questionFilter.questionId != questionId) return false
return contents.any { questionFilter.contents.contains(it) }
}
fun isMatched(questionFilter: QuestionFilter) = contents.any { questionFilter.contents.contains(it) }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
package com.sbl.sulmun2yong.survey.domain.result

import com.sbl.sulmun2yong.survey.exception.InvalidResultFilterException

data class ResultFilter(
val questionFilters: List<QuestionFilter>,
)
) {
companion object {
const val MAX_SIZE = 20
}

init {
require(questionFilters.size <= MAX_SIZE) { throw InvalidResultFilterException() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,47 @@ package com.sbl.sulmun2yong.survey.domain.result
import java.util.UUID

data class SurveyResult(
val resultDetails: List<ResultDetails>,
val questionResults: List<QuestionResult>,
) {
fun getFilteredResult(resultFilter: ResultFilter): SurveyResult {
val questionFilters = resultFilter.questionFilters
if (questionFilters.isEmpty()) return this
var filteredSurveyResult = copy()
for (questionFilter in questionFilters) {
filteredSurveyResult = filteredSurveyResult.filterByQuestionFilter(questionFilter)
if (filteredSurveyResult.resultDetails.isEmpty()) return filteredSurveyResult
val targetQuestionResult = findQuestionResult(questionFilter.questionId) ?: continue
val participantSet = targetQuestionResult.getMatchedParticipants(questionFilter)
filteredSurveyResult = filteredSurveyResult.filterByQuestionFilter(participantSet, questionFilter)
}
return filteredSurveyResult
}

fun findResultDetailsByQuestionId(questionId: UUID) = resultDetails.filter { it.questionId == questionId }
fun findQuestionResult(questionId: UUID) = questionResults.find { it.questionId == questionId }

private fun filterByQuestionFilter(questionFilter: QuestionFilter): SurveyResult {
val participantSet = getMatchedParticipants(questionFilter)
return if (questionFilter.isPositive) {
// isPositive가 true이면 해당 참가자들을 포함
SurveyResult(resultDetails.filter { participantSet.contains(it.participantId) })
} else {
// isPositive가 false이면 해당 참가자들을 제외
SurveyResult(resultDetails.filter { !participantSet.contains(it.participantId) })
}
}
fun getParticipantCount() =
questionResults
.map { it.resultDetails.map { resultDetail -> resultDetail.participantId } }
.flatten()
.toSet()
.size

private fun getMatchedParticipants(questionFilter: QuestionFilter): Set<UUID> =
resultDetails
.mapNotNull { response ->
if (response.isMatched(questionFilter)) response.participantId else null
}.toSet()
private fun filterByQuestionFilter(
participantSet: Set<UUID>,
questionFilter: QuestionFilter,
): SurveyResult =
SurveyResult(
questionResults.map { questionResult ->
val responseDetails =
if (questionFilter.isPositive) {
questionResult.resultDetails.filter {
participantSet.contains(it.participantId)
}
} else {
questionResult.resultDetails.filter { !participantSet.contains(it.participantId) }
}
QuestionResult(
questionResult.questionId,
responseDetails,
questionResult.contents,
)
},
)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package com.sbl.sulmun2yong.survey.dto.response

import com.sbl.sulmun2yong.survey.domain.Survey
import com.sbl.sulmun2yong.survey.domain.question.ChoiceQuestion
import com.sbl.sulmun2yong.survey.domain.question.Question
import com.sbl.sulmun2yong.survey.domain.question.QuestionType
import com.sbl.sulmun2yong.survey.domain.result.ResultDetails
import com.sbl.sulmun2yong.survey.domain.result.QuestionResult
import com.sbl.sulmun2yong.survey.domain.result.SurveyResult
import com.sbl.sulmun2yong.survey.domain.section.Section
import java.util.UUID

data class SurveyResultResponse(
val sectionResults: List<SectionResultResponse>,
val participantCount: Int,
) {
companion object {
fun of(
surveyResult: SurveyResult,
survey: Survey,
) = SurveyResultResponse(survey.sections.map { SectionResultResponse.of(surveyResult, it) })
) = SurveyResultResponse(survey.sections.map { SectionResultResponse.of(surveyResult, it) }, surveyResult.getParticipantCount())
}

data class SectionResultResponse(
Expand All @@ -32,11 +34,9 @@ data class SurveyResultResponse(
sectionId = section.id.value,
title = section.title,
questionResults =
section.questions.map {
QuestionResultResponse.of(
question = it,
responses = surveyResult.findResultDetailsByQuestionId(it.id),
)
section.questions.mapNotNull { question ->
val questionResult = surveyResult.findQuestionResult(question.id)
questionResult?.let { QuestionResultResponse.of(question, it) }
},
)
}
Expand All @@ -48,27 +48,39 @@ data class SurveyResultResponse(
val type: QuestionType,
val participantCount: Int,
val responses: List<Response>,
val responseContents: List<String>,
) {
companion object {
fun of(
question: Question,
responses: List<ResultDetails>,
questionResult: QuestionResult,
): QuestionResultResponse {
val contentCountMap =
responses
.map { it.contents }
.flatten()
.groupingBy { it }
.eachCount()
.toMutableMap()
val contents = question.choices?.standardChoices?.map { it.content } ?: emptyList()
contents.forEach { contentCountMap.putIfAbsent(it, 0) }
val allContents =
if (question is ChoiceQuestion) {
val tempContents = question.choices.standardChoices.map { it.content }
tempContents + questionResult.contents.filter { !tempContents.contains(it) }
} else {
questionResult.contents.toList()
}

val responses =
if (question is ChoiceQuestion) {
val contentCountMap =
questionResult.resultDetails
.flatMap { it.contents }
.groupingBy { it }
.eachCount()
allContents.map { Response(it, contentCountMap[it] ?: 0) }
} else {
questionResult.resultDetails.map { Response(it.contents.first(), 1) }
}
return QuestionResultResponse(
questionId = question.id,
title = question.title,
type = question.questionType,
participantCount = responses.size,
responses = contentCountMap.map { Response(it.key, it.value) },
participantCount = questionResult.resultDetails.size,
responses = responses,
responseContents = allContents,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sbl.sulmun2yong.survey.exception

import com.sbl.sulmun2yong.global.error.BusinessException
import com.sbl.sulmun2yong.global.error.ErrorCode

class InvalidResultFilterException : BusinessException(ErrorCode.INVALID_RESULT_FILTER)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sbl.sulmun2yong.fixture.survey

import com.sbl.sulmun2yong.survey.domain.result.QuestionFilter
import com.sbl.sulmun2yong.survey.domain.result.QuestionResult
import com.sbl.sulmun2yong.survey.domain.result.ResultDetails
import com.sbl.sulmun2yong.survey.domain.result.SurveyResult
import java.util.UUID
Expand All @@ -24,22 +25,18 @@ object SurveyResultConstFactory {
val PARTICIPANT_RESULT_DETAILS_1 =
listOf(
ResultDetails(
questionId = JOB_QUESTION_ID,
participantId = PARTICIPANT_ID_1,
contents = listOf(JOB_QUESTION_CONTENTS[0]),
),
ResultDetails(
questionId = GENDER_QUESTION_ID,
participantId = PARTICIPANT_ID_1,
contents = listOf(GENDER_QUESTION_CONTENTS[0]),
),
ResultDetails(
questionId = FOOD_MULTIPLE_CHOICE_QUESTION_ID,
participantId = PARTICIPANT_ID_1,
contents = listOf(FOOD_MULTIPLE_CHOICE_QUESTION_CONTENTS[0], FOOD_MULTIPLE_CHOICE_QUESTION_CONTENTS[1]),
),
ResultDetails(
questionId = FOOD_TEXT_RESPONSE_QUESTION_ID,
participantId = PARTICIPANT_ID_1,
contents = listOf(FOOD_TEXT_RESPONSE_QUESTION_CONTENTS[0]),
),
Expand All @@ -51,17 +48,14 @@ object SurveyResultConstFactory {
val PARTICIPANT_RESULT_DETAILS_2 =
listOf(
ResultDetails(
questionId = JOB_QUESTION_ID,
participantId = PARTICIPANT_ID_2,
contents = listOf(JOB_QUESTION_CONTENTS[1]),
),
ResultDetails(
questionId = GENDER_QUESTION_ID,
participantId = PARTICIPANT_ID_2,
contents = listOf(GENDER_QUESTION_CONTENTS[1]),
),
ResultDetails(
questionId = FOOD_MULTIPLE_CHOICE_QUESTION_ID,
participantId = PARTICIPANT_ID_2,
contents =
listOf(
Expand All @@ -71,7 +65,6 @@ object SurveyResultConstFactory {
),
),
ResultDetails(
questionId = FOOD_TEXT_RESPONSE_QUESTION_ID,
participantId = PARTICIPANT_ID_2,
contents = listOf(FOOD_TEXT_RESPONSE_QUESTION_CONTENTS[1]),
),
Expand All @@ -83,32 +76,53 @@ object SurveyResultConstFactory {
val PARTICIPANT_RESULT_DETAILS_3 =
listOf(
ResultDetails(
questionId = JOB_QUESTION_ID,
participantId = PARTICIPANT_ID_3,
contents = listOf(JOB_QUESTION_CONTENTS[0]),
),
ResultDetails(
questionId = GENDER_QUESTION_ID,
participantId = PARTICIPANT_ID_3,
contents = listOf(GENDER_QUESTION_CONTENTS[1]),
),
ResultDetails(
questionId = FOOD_MULTIPLE_CHOICE_QUESTION_ID,
participantId = PARTICIPANT_ID_3,
contents = listOf(FOOD_MULTIPLE_CHOICE_QUESTION_CONTENTS[2], FOOD_MULTIPLE_CHOICE_QUESTION_CONTENTS[4]),
),
ResultDetails(
questionId = FOOD_TEXT_RESPONSE_QUESTION_ID,
participantId = PARTICIPANT_ID_3,
contents = listOf(FOOD_TEXT_RESPONSE_QUESTION_CONTENTS[2]),
),
)

val SURVEY_RESULT =
SurveyResult(
resultDetails = PARTICIPANT_RESULT_DETAILS_1 + PARTICIPANT_RESULT_DETAILS_2 + PARTICIPANT_RESULT_DETAILS_3,
val QUESTION_RESULT_1 =
QuestionResult(
questionId = JOB_QUESTION_ID,
resultDetails = listOf(PARTICIPANT_RESULT_DETAILS_1[0], PARTICIPANT_RESULT_DETAILS_2[0], PARTICIPANT_RESULT_DETAILS_3[0]),
contents = JOB_QUESTION_CONTENTS.toSortedSet(),
)

val QUESTION_RESULT_2 =
QuestionResult(
questionId = GENDER_QUESTION_ID,
resultDetails = listOf(PARTICIPANT_RESULT_DETAILS_1[1], PARTICIPANT_RESULT_DETAILS_2[1], PARTICIPANT_RESULT_DETAILS_3[1]),
contents = GENDER_QUESTION_CONTENTS.toSortedSet(),
)

val QUESTION_RESULT_3 =
QuestionResult(
questionId = FOOD_MULTIPLE_CHOICE_QUESTION_ID,
resultDetails = listOf(PARTICIPANT_RESULT_DETAILS_1[2], PARTICIPANT_RESULT_DETAILS_2[2], PARTICIPANT_RESULT_DETAILS_3[2]),
contents = FOOD_MULTIPLE_CHOICE_QUESTION_CONTENTS.toSortedSet(),
)

val QUESTION_RESULT_4 =
QuestionResult(
questionId = FOOD_TEXT_RESPONSE_QUESTION_ID,
resultDetails = listOf(PARTICIPANT_RESULT_DETAILS_1[3], PARTICIPANT_RESULT_DETAILS_2[3], PARTICIPANT_RESULT_DETAILS_3[3]),
contents = FOOD_TEXT_RESPONSE_QUESTION_CONTENTS.toSortedSet(),
)

val SURVEY_RESULT = SurveyResult(questionResults = listOf(QUESTION_RESULT_1, QUESTION_RESULT_2, QUESTION_RESULT_3, QUESTION_RESULT_4))

val EXCEPT_STUDENT_FILTER = QuestionFilter(JOB_QUESTION_ID, listOf(JOB_QUESTION_CONTENTS[0]), false)
val MAN_FILTER = QuestionFilter(GENDER_QUESTION_ID, listOf(GENDER_QUESTION_CONTENTS[0]), true)
val K_J_FOOD_FILTER =
Expand Down
Loading

0 comments on commit a96ec0a

Please sign in to comment.