Skip to content

Commit

Permalink
Merge pull request #67 from SUIN-BUNDANG-LINE/SBL-144-survey-validati…
Browse files Browse the repository at this point in the history
…on-fix

[SBL-144] 설문 유효성 검증 로직 수정
  • Loading branch information
GulSauce authored Sep 24, 2024
2 parents 67200a2 + 8128e9a commit dcaddc4
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 71 deletions.
27 changes: 25 additions & 2 deletions src/main/kotlin/com/sbl/sulmun2yong/survey/domain/Survey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.sbl.sulmun2yong.survey.exception.InvalidSurveyException
import com.sbl.sulmun2yong.survey.exception.InvalidSurveyResponseException
import com.sbl.sulmun2yong.survey.exception.InvalidSurveyStartException
import com.sbl.sulmun2yong.survey.exception.InvalidUpdateSurveyException
import com.sbl.sulmun2yong.survey.exception.SurveyClosedException
import java.util.Date
import java.util.UUID

Expand All @@ -32,14 +33,19 @@ data class Survey(
val sections: List<Section>,
) {
init {
require(sections.isNotEmpty()) { throw InvalidSurveyException() }
require(isSectionsUnique()) { throw InvalidSurveyException() }
require(isSurveyStatusValid()) { throw InvalidSurveyException() }
require(isFinishedAtAfterPublishedAt()) { throw InvalidPublishedAtException() }
require(isSectionIdsValid()) { throw InvalidSurveyException() }
// 설문이 진행 중인 경우만 섹션이 비었는지, 선택지가 중복되는지 확인
if (status == SurveyStatus.IN_PROGRESS) {
require(sections.isNotEmpty()) { throw InvalidSurveyException() }
require(isAllChoicesUnique()) { throw InvalidSurveyException() }
}
}

companion object {
// TODO: 기본 섬네일 URL은 프론트에서 처리하도록 변경
const val DEFAULT_THUMBNAIL_URL = "https://test-oriddle-bucket.s3.ap-northeast-2.amazonaws.com/surveyImage.webp"
const val DEFAULT_TITLE = "제목 없는 설문"
const val DEFAULT_DESCRIPTION = ""
Expand All @@ -63,6 +69,8 @@ data class Survey(

/** 설문의 응답 순서가 유효한지, 응답이 각 섹션에 유효한지 확인하는 메서드 */
fun validateResponse(surveyResponse: SurveyResponse) {
// 진행 중인 설문이 아니면 응답이 유효한지 확인할 수 없다.
require(status == SurveyStatus.IN_PROGRESS) { throw SurveyClosedException() }
// 확인할 응답의 예상 섹션 ID, 첫 응답의 섹션 ID는 첫 섹션의 ID
var expectedSectionId: SectionId = sections.first().id
for (sectionResponse in surveyResponse) {
Expand Down Expand Up @@ -111,7 +119,19 @@ data class Survey(
/** 설문을 IN_PROGRESS 상태로 변경하는 메서드. 설문이 시작 전이거나 수정 중인 경우만 가능하다. */
fun start() =
when (status) {
SurveyStatus.NOT_STARTED -> copy(status = SurveyStatus.IN_PROGRESS, publishedAt = DateUtil.getCurrentDate())
SurveyStatus.NOT_STARTED ->
copy(
status = SurveyStatus.IN_PROGRESS,
publishedAt = DateUtil.getCurrentDate(),
rewardSetting =
RewardSetting.of(
type = rewardSetting.type,
rewards = rewardSetting.rewards,
targetParticipantCount = rewardSetting.targetParticipantCount,
finishedAt = rewardSetting.finishedAt?.value,
surveyStatus = SurveyStatus.IN_PROGRESS,
),
)
SurveyStatus.IN_MODIFICATION -> copy(status = SurveyStatus.IN_PROGRESS)
SurveyStatus.IN_PROGRESS -> throw InvalidSurveyStartException()
SurveyStatus.CLOSED -> throw InvalidSurveyStartException()
Expand All @@ -136,7 +156,10 @@ data class Survey(
}

private fun isSectionIdsValid(): Boolean {
if (sections.isEmpty()) return true
val sectionIds = SectionIds.from(sections.map { it.id })
return sections.all { it.sectionIds == sectionIds }
}

private fun isAllChoicesUnique() = sections.all { section -> section.questions.all { it.choices?.isUnique() ?: true } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ data class Choices(
) {
init {
if (standardChoices.isEmpty()) throw InvalidChoiceException()
if (standardChoices.size != standardChoices.distinct().size) throw InvalidChoiceException()
}

/** 응답이 선택지에 포함되는지 확인 */
Expand All @@ -22,4 +21,6 @@ data class Choices(

/** 선택지 기반 라우팅의 선택지와 같은지 비교하기 위해 선택지 집합을 얻는다. */
fun getChoiceSet() = if (isAllowOther) standardChoices.toSet() + Choice.Other else standardChoices.toSet()

fun isUnique() = standardChoices.size == standardChoices.distinct().size
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.sbl.sulmun2yong.survey.domain.reward

/** 시작 전 상태의 불완전한 리워드 지급 설정 */
data class NotStartedDrawSetting(
override val type: RewardSettingType,
override val rewards: List<Reward>,
override val targetParticipantCount: Int?,
override val finishedAt: FinishedAt?,
) : RewardSetting
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.sbl.sulmun2yong.survey.domain.reward

import com.sbl.sulmun2yong.survey.domain.SurveyStatus
import com.sbl.sulmun2yong.survey.exception.InvalidRewardSettingException
import java.util.Date

Expand All @@ -18,31 +19,26 @@ interface RewardSetting {
rewards: List<Reward>,
targetParticipantCount: Int?,
finishedAt: Date?,
) = when (type) {
RewardSettingType.NO_REWARD -> {
if (rewards.isEmpty() && targetParticipantCount == null && finishedAt == null) {
NoRewardSetting
} else {
surveyStatus: SurveyStatus = SurveyStatus.IN_PROGRESS,
): RewardSetting {
if (surveyStatus == SurveyStatus.NOT_STARTED) {
val finishedAtValue = finishedAt?.let { FinishedAt(it) }
return NotStartedDrawSetting(type, rewards, targetParticipantCount, finishedAtValue)
}
when (type) {
RewardSettingType.NO_REWARD -> {
val isNoReward = rewards.isEmpty() && targetParticipantCount == null && finishedAt == null
if (isNoReward) return NoRewardSetting
throw InvalidRewardSettingException()
}
}
RewardSettingType.SELF_MANAGEMENT -> {
if (rewards.isNotEmpty() &&
targetParticipantCount == null &&
finishedAt != null
) {
SelfManagementSetting(rewards, FinishedAt(finishedAt))
} else {
RewardSettingType.SELF_MANAGEMENT -> {
val isSelfManagement = rewards.isNotEmpty() && targetParticipantCount == null && finishedAt != null
if (isSelfManagement) return SelfManagementSetting(rewards, FinishedAt(finishedAt!!))
throw InvalidRewardSettingException()
}
}
RewardSettingType.IMMEDIATE_DRAW -> {
if (rewards.isNotEmpty() &&
targetParticipantCount != null &&
finishedAt != null
) {
ImmediateDrawSetting(rewards, targetParticipantCount, FinishedAt(finishedAt))
} else {
RewardSettingType.IMMEDIATE_DRAW -> {
val isImmediateDraw = rewards.isNotEmpty() && targetParticipantCount != null && finishedAt != null
if (isImmediateDraw) return ImmediateDrawSetting(rewards, targetParticipantCount!!, FinishedAt(finishedAt!!))
throw InvalidRewardSettingException()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.sbl.sulmun2yong.survey.dto.request

import com.sbl.sulmun2yong.survey.domain.SurveyStatus
import com.sbl.sulmun2yong.survey.domain.question.QuestionType
import com.sbl.sulmun2yong.survey.domain.question.choice.Choice
import com.sbl.sulmun2yong.survey.domain.question.choice.Choices
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardMultipleChoiceQuestion
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardSingleChoiceQuestion
import com.sbl.sulmun2yong.survey.domain.question.impl.StandardTextQuestion
import com.sbl.sulmun2yong.survey.domain.reward.Reward
import com.sbl.sulmun2yong.survey.domain.reward.RewardSetting
import com.sbl.sulmun2yong.survey.domain.reward.RewardSettingType
import com.sbl.sulmun2yong.survey.domain.routing.RoutingStrategy
import com.sbl.sulmun2yong.survey.domain.routing.RoutingType
Expand All @@ -25,12 +28,38 @@ data class SurveySaveRequest(
val rewardSetting: RewardSettingResponse,
val sections: List<SectionCreateRequest>,
) {
fun List<SectionCreateRequest>.toDomain() =
if (isEmpty()) {
listOf()
} else {
val sectionIds = SectionIds.from(this.map { SectionId.Standard(it.sectionId) })
this.map {
Section(
id = SectionId.Standard(it.sectionId),
title = it.title,
description = it.description,
routingStrategy = it.getRoutingStrategy(),
questions = it.questions.map { question -> question.toDomain() },
sectionIds = sectionIds,
)
}
}

data class RewardSettingResponse(
val type: RewardSettingType,
val rewards: List<RewardCreateRequest>,
val targetParticipantCount: Int?,
val finishedAt: Date?,
)
) {
fun toDomain(surveyStatus: SurveyStatus) =
RewardSetting.of(
type,
rewards.map { Reward(it.name, it.category, it.count) },
targetParticipantCount,
finishedAt,
surveyStatus,
)
}

data class RewardCreateRequest(
val name: String,
Expand All @@ -45,17 +74,7 @@ data class SurveySaveRequest(
val questions: List<QuestionCreateRequest>,
val routeDetails: RouteDetailsCreateRequest,
) {
fun toDomain(sectionIds: SectionIds) =
Section(
id = SectionId.Standard(sectionId),
title = title,
description = description,
routingStrategy = getRoutingStrategy(),
questions = questions.map { it.toDomain() },
sectionIds = sectionIds,
)

private fun getRoutingStrategy() =
fun getRoutingStrategy() =
when (routeDetails.type) {
RoutingType.NUMERICAL_ORDER -> RoutingStrategy.NumericalOrder
RoutingType.SET_BY_USER -> RoutingStrategy.SetByUser(SectionId.from(routeDetails.nextSectionId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,13 @@ data class SurveyDocument(
)

fun toDomain(): Survey {
val sectionIds = SectionIds.from(this.sections.map { SectionId.Standard(it.sectionId) })
val sections =
if (this.sections.isEmpty()) {
listOf()
} else {
val sectionIds = SectionIds.from(this.sections.map { SectionId.Standard(it.sectionId) })
this.sections.map { it.toDomain(sectionIds) }
}
return Survey(
id = this.id,
title = this.title,
Expand All @@ -167,13 +173,22 @@ data class SurveyDocument(
this.rewards.map { it.toDomain() },
this.targetParticipantCount,
this.finishedAt,
this.status,
),
isVisible = this.isVisible,
makerId = this.makerId,
sections = this.sections.map { it.toDomain(sectionIds) },
sections = this.sections.toDomain(),
)
}

private fun List<SectionSubDocument>.toDomain() =
if (this.isEmpty()) {
listOf()
} else {
val sectionIds = SectionIds.from(this.map { SectionId.Standard(it.sectionId) })
this.map { it.toDomain(sectionIds) }
}

private fun RewardSubDocument.toDomain() =
Reward(
name = this.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import com.sbl.sulmun2yong.drawing.domain.DrawingBoard
import com.sbl.sulmun2yong.survey.adapter.SurveyAdapter
import com.sbl.sulmun2yong.survey.domain.Survey
import com.sbl.sulmun2yong.survey.domain.reward.ImmediateDrawSetting
import com.sbl.sulmun2yong.survey.domain.reward.Reward
import com.sbl.sulmun2yong.survey.domain.reward.RewardSetting
import com.sbl.sulmun2yong.survey.domain.section.SectionId
import com.sbl.sulmun2yong.survey.domain.section.SectionIds
import com.sbl.sulmun2yong.survey.dto.request.SurveySaveRequest
import com.sbl.sulmun2yong.survey.dto.response.SurveyCreateResponse
import com.sbl.sulmun2yong.survey.dto.response.SurveyMakeInfoResponse
Expand Down Expand Up @@ -36,24 +32,16 @@ class SurveyManagementService(
val survey = surveyAdapter.getSurvey(surveyId)
// 현재 유저와 설문 제작자가 다를 경우 예외 발생
if (survey.makerId != makerId) throw InvalidSurveyAccessException()
val rewards = surveySaveRequest.rewardSetting.rewards.map { Reward(name = it.name, category = it.category, count = it.count) }
val newSurvey =
with(surveySaveRequest) {
val sectionIds = SectionIds.from(surveySaveRequest.sections.map { SectionId.Standard(it.sectionId) })
survey.updateContent(
title = this.title,
description = this.description,
thumbnail = this.thumbnail,
finishMessage = this.finishMessage,
rewardSetting =
RewardSetting.of(
this.rewardSetting.type,
rewards,
this.rewardSetting.targetParticipantCount,
this.rewardSetting.finishedAt,
),
rewardSetting = this.rewardSetting.toDomain(survey.status),
isVisible = this.isVisible,
sections = this.sections.map { it.toDomain(sectionIds) },
sections = this.sections.toDomain(),
)
}
surveyAdapter.save(newSurvey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import com.sbl.sulmun2yong.survey.adapter.ParticipantAdapter
import com.sbl.sulmun2yong.survey.adapter.ResponseAdapter
import com.sbl.sulmun2yong.survey.adapter.SurveyAdapter
import com.sbl.sulmun2yong.survey.domain.Participant
import com.sbl.sulmun2yong.survey.domain.SurveyStatus
import com.sbl.sulmun2yong.survey.dto.request.SurveyResponseRequest
import com.sbl.sulmun2yong.survey.dto.response.SurveyParticipantResponse
import com.sbl.sulmun2yong.survey.exception.SurveyClosedException
import org.springframework.stereotype.Service
import java.util.UUID

Expand All @@ -23,9 +21,6 @@ class SurveyResponseService(
surveyResponseRequest: SurveyResponseRequest,
): SurveyParticipantResponse {
val survey = surveyAdapter.getSurvey(surveyId)
if (survey.status != SurveyStatus.IN_PROGRESS) {
throw SurveyClosedException()
}
val surveyResponse = surveyResponseRequest.toDomain(surveyId)
survey.validateResponse(surveyResponse)
// TODO: 참가자 객체의 UserId에 실제 유저 값 넣기
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ object SurveyFixtureFactory {
status = status,
finishMessage = finishMessage + id,
makerId = makerId,
rewardSetting = createRewardSetting(type, rewards, targetParticipantCount, finishedAt),
rewardSetting = createRewardSetting(type, rewards, targetParticipantCount, finishedAt, status),
isVisible = isVisible,
sections = sections,
)
Expand All @@ -77,5 +77,6 @@ object SurveyFixtureFactory {
rewards: List<Reward> = REWARDS,
targetParticipantCount: Int? = TARGET_PARTICIPANT_COUNT,
finishedAt: Date? = FINISHED_AT,
) = RewardSetting.of(type, rewards, targetParticipantCount, finishedAt)
status: SurveyStatus = SURVEY_STATUS,
) = RewardSetting.of(type, rewards, targetParticipantCount, finishedAt, status)
}
Loading

0 comments on commit dcaddc4

Please sign in to comment.