diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt index 8fd32d185..c97f38cc6 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt @@ -6,10 +6,8 @@ import com.few.api.repo.dao.article.command.InsertFullArticleRecordCommand import com.few.api.repo.dao.article.query.* import com.few.api.repo.dao.article.record.* import com.few.data.common.code.MemberType -import jooq.jooq_dsl.tables.ArticleIfo -import jooq.jooq_dsl.tables.ArticleMst -import jooq.jooq_dsl.tables.MappingWorkbookArticle -import jooq.jooq_dsl.tables.Member +import jooq.jooq_dsl.tables.* +import jooq.jooq_dsl.tables.MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE import org.jooq.* import org.jooq.impl.DSL import org.springframework.cache.annotation.Cacheable @@ -163,4 +161,23 @@ class ArticleDao( ) .where(ArticleIfo.ARTICLE_IFO.ARTICLE_MST_ID.eq(query.articleId)) .and(ArticleIfo.ARTICLE_IFO.DELETED_AT.isNull) + + fun selectArticleIdsByWorkbookIdLimitDay(query: SelectAritlceIdByWorkbookIdAndDayQuery): ArticleIdRecord { + return selectArticleIdByWorkbookIdLimitDayQuery(query) + .fetch() + .map { it[MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID] } + .let { ArticleIdRecord(it) } + } + + fun selectArticleIdByWorkbookIdLimitDayQuery(query: SelectAritlceIdByWorkbookIdAndDayQuery) = + dslContext + .select(MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID) + .from(MAPPING_WORKBOOK_ARTICLE) + .join(ArticleMst.ARTICLE_MST) + .on(MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID.eq(ArticleMst.ARTICLE_MST.ID)) + .where(MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(query.workbookId)) + .and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull) + .orderBy(MAPPING_WORKBOOK_ARTICLE.DAY_COL.asc()) + .limit(query.day) + .query } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/query/SelectAritlceIdByWorkbookIdAndDayQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/query/SelectAritlceIdByWorkbookIdAndDayQuery.kt new file mode 100644 index 000000000..9f2be69ee --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/query/SelectAritlceIdByWorkbookIdAndDayQuery.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.article.query + +data class SelectAritlceIdByWorkbookIdAndDayQuery( + val workbookId: Long, + val day: Int, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleIdRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleIdRecord.kt new file mode 100644 index 000000000..ecb7cff77 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleIdRecord.kt @@ -0,0 +1,5 @@ +package com.few.api.repo.dao.article.record + +data class ArticleIdRecord( + val articleIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt index a10dc6049..844f6ce6a 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt @@ -2,8 +2,10 @@ package com.few.api.repo.dao.problem import com.few.api.repo.dao.problem.command.InsertProblemsCommand import com.few.api.repo.dao.problem.query.SelectProblemAnswerQuery +import com.few.api.repo.dao.problem.query.SelectProblemIdByArticleIdsQuery import com.few.api.repo.dao.problem.query.SelectProblemQuery import com.few.api.repo.dao.problem.query.SelectProblemsByArticleIdQuery +import com.few.api.repo.dao.problem.record.ProblemIdAndArticleIdRecord import com.few.api.repo.dao.problem.record.ProblemIdsRecord import com.few.api.repo.dao.problem.record.SelectProblemAnswerRecord import com.few.api.repo.dao.problem.record.SelectProblemRecord @@ -28,7 +30,8 @@ class ProblemDao( Problem.PROBLEM.ID.`as`(SelectProblemRecord::id.name), Problem.PROBLEM.TITLE.`as`(SelectProblemRecord::title.name), DSL.field("JSON_UNQUOTE({0})", String::class.java, Problem.PROBLEM.CONTENTS) - .`as`(SelectProblemRecord::contents.name) + .`as`(SelectProblemRecord::contents.name), + Problem.PROBLEM.ARTICLE_ID.`as`(SelectProblemRecord::articleId.name) ) .from(Problem.PROBLEM) .where(Problem.PROBLEM.ID.eq(query.problemId)) @@ -77,4 +80,18 @@ class ProblemDao( .set(Problem.PROBLEM.CONTENTS, JSON.valueOf(contentsJsonMapper.toJson(it.contents))) .set(Problem.PROBLEM.ANSWER, it.answer) .set(Problem.PROBLEM.EXPLANATION, it.explanation) + + fun selectProblemIdByArticleIds(query: SelectProblemIdByArticleIdsQuery): List { + return selectProblemIdByArticleIdsQuery(query) + .fetchInto(ProblemIdAndArticleIdRecord::class.java) + } + + fun selectProblemIdByArticleIdsQuery(query: SelectProblemIdByArticleIdsQuery) = dslContext + .select( + Problem.PROBLEM.ID.`as`(ProblemIdAndArticleIdRecord::problemId.name), + Problem.PROBLEM.ARTICLE_ID.`as`(ProblemIdAndArticleIdRecord::articleId.name) + ) + .from(Problem.PROBLEM) + .where(Problem.PROBLEM.ARTICLE_ID.`in`(query.articleIds)) + .and(Problem.PROBLEM.DELETED_AT.isNull) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/SubmitHistoryDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/SubmitHistoryDao.kt index e63482826..7eab14aba 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/SubmitHistoryDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/SubmitHistoryDao.kt @@ -1,6 +1,8 @@ package com.few.api.repo.dao.problem import com.few.api.repo.dao.problem.command.InsertSubmitHistoryCommand +import com.few.api.repo.dao.problem.query.SelectSubmittedProblemIdsQuery +import com.few.api.repo.dao.problem.record.SubmittedProblemIdsRecord import jooq.jooq_dsl.Tables.SUBMIT_HISTORY import org.jooq.DSLContext import org.springframework.stereotype.Repository @@ -24,4 +26,18 @@ class SubmitHistoryDao( .set(SUBMIT_HISTORY.MEMBER_ID, command.memberId) .set(SUBMIT_HISTORY.SUBMIT_ANS, command.submitAns) .set(SUBMIT_HISTORY.IS_SOLVED, command.isSolved) + + fun selectProblemIdByProblemIds(query: SelectSubmittedProblemIdsQuery): SubmittedProblemIdsRecord { + return selectProblemIdByProblemIdsQuery(query) + .fetch() + .map { it[SUBMIT_HISTORY.PROBLEM_ID] } + .let { SubmittedProblemIdsRecord(it) } + } + + fun selectProblemIdByProblemIdsQuery(query: SelectSubmittedProblemIdsQuery) = dslContext + .select(SUBMIT_HISTORY.PROBLEM_ID) + .from(SUBMIT_HISTORY) + .where(SUBMIT_HISTORY.PROBLEM_ID.`in`(query.problemIds)) + .and(SUBMIT_HISTORY.MEMBER_ID.eq(query.memberId)) + .and(SUBMIT_HISTORY.DELETED_AT.isNull) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectProblemIdByArticleIdsQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectProblemIdByArticleIdsQuery.kt new file mode 100644 index 000000000..2511f6f21 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectProblemIdByArticleIdsQuery.kt @@ -0,0 +1,5 @@ +package com.few.api.repo.dao.problem.query + +data class SelectProblemIdByArticleIdsQuery( + val articleIds: Set, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectSubmittedProblemIdsQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectSubmittedProblemIdsQuery.kt new file mode 100644 index 000000000..902993fd5 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/query/SelectSubmittedProblemIdsQuery.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.problem.query + +data class SelectSubmittedProblemIdsQuery( + val memberId: Long, + val problemIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/ProblemIdAndArticleIdRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/ProblemIdAndArticleIdRecord.kt new file mode 100644 index 000000000..bf4b0d535 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/ProblemIdAndArticleIdRecord.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.problem.record + +data class ProblemIdAndArticleIdRecord( + val problemId: Long, + val articleId: Long, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SelectProblemRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SelectProblemRecord.kt index 3852d5044..2066cbf25 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SelectProblemRecord.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SelectProblemRecord.kt @@ -4,4 +4,5 @@ data class SelectProblemRecord( val id: Long, val title: String, val contents: String, + val articleId: Long, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SubmittedProblemIdsRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SubmittedProblemIdsRecord.kt new file mode 100644 index 000000000..f70ca92cc --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/record/SubmittedProblemIdsRecord.kt @@ -0,0 +1,5 @@ +package com.few.api.repo.dao.problem.record + +data class SubmittedProblemIdsRecord( + val problemIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt index 20e0fb2da..5316134c3 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt @@ -302,4 +302,18 @@ class SubscriptionDao( .from(SUBSCRIPTION) .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) .and(SUBSCRIPTION.DELETED_AT.isNull) + + fun selectWorkbookIdAndProgressByMember(query: SelectSubscriptionSendStatusQuery): List { + return selectWorkbookIdAndProgressByMemberQuery(query) + .fetchInto(SubscriptionProgressRecord::class.java) + } + + fun selectWorkbookIdAndProgressByMemberQuery(query: SelectSubscriptionSendStatusQuery) = + dslContext.select( + SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(SubscriptionProgressRecord::workbookId.name), + SUBSCRIPTION.PROGRESS.add(1).`as`(SubscriptionProgressRecord::day.name) + ) + .from(SUBSCRIPTION) + .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) + .and(SUBSCRIPTION.DELETED_AT.isNull) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionProgressRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionProgressRecord.kt new file mode 100644 index 000000000..1fbf4acf4 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionProgressRecord.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.subscription.record + +data class SubscriptionProgressRecord( + val workbookId: Long, + val day: Int, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/service/ArticleService.kt b/api/src/main/kotlin/com/few/api/domain/problem/service/ArticleService.kt new file mode 100644 index 000000000..966ffc46b --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/service/ArticleService.kt @@ -0,0 +1,21 @@ +package com.few.api.domain.problem.service + +import com.few.api.domain.problem.service.dto.BrowseArticleIdInDto +import com.few.api.repo.dao.article.ArticleDao +import com.few.api.repo.dao.article.query.SelectAritlceIdByWorkbookIdAndDayQuery +import org.springframework.stereotype.Service + +@Service +class ArticleService( + private val articleDao: ArticleDao, +) { + + fun browseArticleIdByWorkbookIdLimitDay(inDto: BrowseArticleIdInDto): List { + return articleDao.selectArticleIdsByWorkbookIdLimitDay( + SelectAritlceIdByWorkbookIdAndDayQuery( + inDto.workbookId, + inDto.day + ) + ).articleIds + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/service/SubscriptionService.kt b/api/src/main/kotlin/com/few/api/domain/problem/service/SubscriptionService.kt new file mode 100644 index 000000000..8a18f9a2c --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/service/SubscriptionService.kt @@ -0,0 +1,21 @@ +package com.few.api.domain.problem.service + +import com.few.api.domain.problem.service.dto.BrowseWorkbookIdAndProgressInDto +import com.few.api.domain.problem.service.dto.SubscriptionProgressOutDto +import com.few.api.repo.dao.subscription.SubscriptionDao +import com.few.api.repo.dao.subscription.query.SelectSubscriptionSendStatusQuery +import org.springframework.stereotype.Component + +@Component +class SubscriptionService( + private val subscriptionDao: SubscriptionDao, +) { + + fun browseWorkbookIdAndProgress(inDto: BrowseWorkbookIdAndProgressInDto): List { + val subscriptionProgresses = subscriptionDao.selectWorkbookIdAndProgressByMember( + SelectSubscriptionSendStatusQuery(inDto.memberId) + ) + + return subscriptionProgresses.map { SubscriptionProgressOutDto(it.workbookId, it.day) } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseArticleIdInDto.kt b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseArticleIdInDto.kt new file mode 100644 index 000000000..580c37a4b --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseArticleIdInDto.kt @@ -0,0 +1,6 @@ +package com.few.api.domain.problem.service.dto + +data class BrowseArticleIdInDto( + val workbookId: Long, + val day: Int, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseWorkbookIdAndProgressInDto.kt b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseWorkbookIdAndProgressInDto.kt new file mode 100644 index 000000000..b2b0438b0 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/BrowseWorkbookIdAndProgressInDto.kt @@ -0,0 +1,5 @@ +package com.few.api.domain.problem.service.dto + +data class BrowseWorkbookIdAndProgressInDto( + val memberId: Long, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/service/dto/SubscriptionProgressOutDto.kt b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/SubscriptionProgressOutDto.kt new file mode 100644 index 000000000..90ff99583 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/service/dto/SubscriptionProgressOutDto.kt @@ -0,0 +1,6 @@ +package com.few.api.domain.problem.service.dto + +data class SubscriptionProgressOutDto( + val workbookId: Long, + val day: Int, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt index 573093ab9..5888bb864 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCase.kt @@ -6,12 +6,14 @@ import com.few.api.exception.common.NotFoundException import com.few.api.repo.dao.problem.ProblemDao import com.few.api.repo.dao.problem.query.SelectProblemsByArticleIdQuery import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component class BrowseProblemsUseCase( private val problemDao: ProblemDao, ) { + @Transactional(readOnly = true) fun execute(useCaseIn: BrowseProblemsUseCaseIn): BrowseProblemsUseCaseOut { problemDao.selectProblemsByArticleId(SelectProblemsByArticleIdQuery(useCaseIn.articleId)) ?.let { diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt new file mode 100644 index 000000000..d9f96e92b --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCase.kt @@ -0,0 +1,82 @@ +package com.few.api.domain.problem.usecase + +import com.few.api.domain.problem.service.ArticleService +import com.few.api.domain.problem.service.SubscriptionService +import com.few.api.domain.problem.service.dto.BrowseArticleIdInDto +import com.few.api.domain.problem.service.dto.BrowseWorkbookIdAndProgressInDto +import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseOut +import com.few.api.domain.problem.usecase.dto.BrowseUndoneProblemsUseCaseIn +import com.few.api.exception.common.NotFoundException +import com.few.api.repo.dao.problem.ProblemDao +import com.few.api.repo.dao.problem.SubmitHistoryDao +import com.few.api.repo.dao.problem.query.SelectProblemIdByArticleIdsQuery +import com.few.api.repo.dao.problem.query.SelectSubmittedProblemIdsQuery +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class BrowseUndoneProblemsUseCase( + private val problemDao: ProblemDao, + private val subscriptionService: SubscriptionService, + private val articleService: ArticleService, + private val submitHistoryDao: SubmitHistoryDao, +) { + + @Transactional(readOnly = true) + fun execute(useCaseIn: BrowseUndoneProblemsUseCaseIn): BrowseProblemsUseCaseOut { + /** + * 유저가 구독한 워크북들에 속한 아티클 개수를 조회함 + * 이때 아티클 개수는 현 시점 기준으로 이메일이 전송된 아티클 개수까지만 조회함 + */ + val subscriptionProgresses = subscriptionService.browseWorkbookIdAndProgress( + BrowseWorkbookIdAndProgressInDto(useCaseIn.memberId) + ).takeIf { it.isNotEmpty() } ?: throw NotFoundException("subscribe.workbook.notexist") + + /** + * 위에서 조회한 워크부에 속한 아티클 개수에 대해 article_id 들을 조회함 + */ + val sentArticleIds = subscriptionProgresses.flatMap { subscriptionProgress -> + articleService.browseArticleIdByWorkbookIdLimitDay( + BrowseArticleIdInDto( + subscriptionProgress.workbookId, + subscriptionProgress.day + ) + ) + }.toSet() + + /** + * 위에서 구한 아티클에 속한 모든 problem_id, article_id 조합을 조회함 + */ + val allProblemIdsAndArticleIdsToBeSolved = problemDao.selectProblemIdByArticleIds( + SelectProblemIdByArticleIdsQuery(sentArticleIds) + ) + + /** + * 위에서 구한 문제들에 대해 풀이 이력이 존재하는 problem_id만 추출 후 + * 유저가 풀어야 할 전체 problem_id에 대해 여집합 연산 + */ + val allProblemIdsToBeSolved = allProblemIdsAndArticleIdsToBeSolved.map { it.problemId } + val submittedProblemIds = submitHistoryDao.selectProblemIdByProblemIds( + SelectSubmittedProblemIdsQuery(useCaseIn.memberId, allProblemIdsToBeSolved) + ).problemIds + + val unsubmittedProblemIdAndArticleIds: Map> = allProblemIdsAndArticleIdsToBeSolved + .filter { it.problemId !in submittedProblemIds } + .groupBy { it.articleId } + .mapValues { entry -> entry.value.map { it.problemId } } + + /** + * 결과를 article_id를 기준으로 랜덤화한 뒤 problem_id를 순차적으로 리턴함 + */ + val randomArticleIds = unsubmittedProblemIdAndArticleIds.keys.shuffled() + val problemIdsRandomizedByArticleId = mutableListOf() + + randomArticleIds.forEach { articleId -> + unsubmittedProblemIdAndArticleIds[articleId]?.let { problemIds -> + problemIdsRandomizedByArticleId.addAll(problemIds) + } + } + + return BrowseProblemsUseCaseOut(problemIdsRandomizedByArticleId) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt index fa5ea0f5d..7b750b14e 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt @@ -33,7 +33,8 @@ class ReadProblemUseCase( return ReadProblemUseCaseOut( id = record.id, title = record.title, - contents = contents + contents = contents, + articleId = record.articleId ) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/BrowseUndoneProblemsUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/BrowseUndoneProblemsUseCaseIn.kt new file mode 100644 index 000000000..21459430c --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/BrowseUndoneProblemsUseCaseIn.kt @@ -0,0 +1,5 @@ +package com.few.api.domain.problem.usecase.dto + +data class BrowseUndoneProblemsUseCaseIn( + val memberId: Long, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/ReadProblemUseCaseOut.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/ReadProblemUseCaseOut.kt index 893f5bc0a..e52b5f5c1 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/ReadProblemUseCaseOut.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/ReadProblemUseCaseOut.kt @@ -4,6 +4,7 @@ class ReadProblemUseCaseOut( val id: Long, val title: String, val contents: List, + val articleId: Long, ) data class ReadProblemContentsUseCaseOutDetail( diff --git a/api/src/main/kotlin/com/few/api/web/controller/problem/ProblemController.kt b/api/src/main/kotlin/com/few/api/web/controller/problem/ProblemController.kt index cc9e366e8..1d9a5a72e 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/problem/ProblemController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/problem/ProblemController.kt @@ -1,6 +1,7 @@ package com.few.api.web.controller.problem import com.few.api.domain.problem.usecase.BrowseProblemsUseCase +import com.few.api.domain.problem.usecase.BrowseUndoneProblemsUseCase import com.few.api.web.controller.problem.request.CheckProblemRequest import com.few.api.web.controller.problem.response.CheckProblemResponse import com.few.api.web.controller.problem.response.ProblemContents @@ -10,6 +11,7 @@ import com.few.api.web.support.ApiResponseGenerator import com.few.api.domain.problem.usecase.CheckProblemUseCase import com.few.api.domain.problem.usecase.ReadProblemUseCase import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseIn +import com.few.api.domain.problem.usecase.dto.BrowseUndoneProblemsUseCaseIn import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseIn import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseIn import com.few.api.web.controller.problem.response.BrowseProblemsResponse @@ -30,6 +32,7 @@ class ProblemController( private val browseProblemsUseCase: BrowseProblemsUseCase, private val readProblemUseCase: ReadProblemUseCase, private val checkProblemUseCase: CheckProblemUseCase, + private val browseUndoneProblemsUseCase: BrowseUndoneProblemsUseCase, ) { @GetMapping @@ -39,7 +42,7 @@ class ProblemController( browseProblemsUseCase.execute(useCaseIn) } - val response = BrowseProblemsResponse(useCaseOut.problemIds) + val response = BrowseProblemsResponse(useCaseOut.problemIds, useCaseOut.problemIds.size) return ApiResponseGenerator.success(response, HttpStatus.OK) } @@ -60,7 +63,8 @@ class ProblemController( title = useCaseOut.title, contents = useCaseOut.contents .map { c -> ProblemContents(c.number, c.content) } - .toCollection(LinkedList()) + .toCollection(LinkedList()), + articleId = useCaseOut.articleId ) return ApiResponseGenerator.success(response, HttpStatus.OK) @@ -92,4 +96,17 @@ class ProblemController( return ApiResponseGenerator.success(response, HttpStatus.OK) } + + @GetMapping("/unsubmitted") + fun browseUndoneProblems(@UserArgument userArgumentDetails: UserArgumentDetails): ApiResponse> { + val memberId = userArgumentDetails.id.toLong() + + val useCaseOut = BrowseUndoneProblemsUseCaseIn(memberId).let { useCaseIn -> + browseUndoneProblemsUseCase.execute(useCaseIn) + } + + val response = BrowseProblemsResponse(useCaseOut.problemIds, useCaseOut.problemIds.size) + + return ApiResponseGenerator.success(response, HttpStatus.OK) + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/problem/response/BrowseProblemsResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/problem/response/BrowseProblemsResponse.kt index 3bc6c1811..b76a8cb04 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/problem/response/BrowseProblemsResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/problem/response/BrowseProblemsResponse.kt @@ -2,4 +2,5 @@ package com.few.api.web.controller.problem.response data class BrowseProblemsResponse( val problemIds: List, + val size: Int? = null, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/problem/response/ReadProblemResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/problem/response/ReadProblemResponse.kt index 691adf602..9c8e3e953 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/problem/response/ReadProblemResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/problem/response/ReadProblemResponse.kt @@ -4,6 +4,7 @@ data class ReadProblemResponse( val id: Long, val title: String, val contents: List, + val articleId: Long, ) data class ProblemContents( diff --git a/api/src/main/resources/messages/subscribe.properties b/api/src/main/resources/messages/subscribe.properties index ee206843d..3f70bdb61 100644 --- a/api/src/main/resources/messages/subscribe.properties +++ b/api/src/main/resources/messages/subscribe.properties @@ -1,2 +1,3 @@ subscribe.state.end=\u0073\u0075\u0062\u0073\u0063\u0072\u0069\u0062\u0065\u002e\u0073\u0074\u0061\u0074\u0065\u002e\u0065\u006e\u0064 subscribe.state.subscribed=\u0073\u0075\u0062\u0073\u0063\u0072\u0069\u0062\u0065\u002e\u0073\u0074\u0061\u0074\u0065\u002e\u0073\u0075\u0062\u0073\u0063\u0072\u0069\u0062\u0065\u0064 +subscribe.workbook.notexist=\u0073\u0075\u0062\u0073\u0063\u0072\u0069\u0062\u0065\u002e\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u002e\u006e\u006f\u0074\u0065\u0078\u0069\u0073\u0074 diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCaseTest.kt new file mode 100644 index 000000000..c9ada1d0f --- /dev/null +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseUndoneProblemsUseCaseTest.kt @@ -0,0 +1,95 @@ +package com.few.api.domain.problem.usecase + +import com.few.api.domain.problem.service.ArticleService +import com.few.api.domain.problem.service.SubscriptionService +import com.few.api.domain.problem.service.dto.SubscriptionProgressOutDto +import com.few.api.domain.problem.usecase.dto.BrowseUndoneProblemsUseCaseIn +import com.few.api.repo.dao.problem.ProblemDao +import com.few.api.repo.dao.problem.SubmitHistoryDao +import com.few.api.repo.dao.problem.record.ProblemIdAndArticleIdRecord +import com.few.api.repo.dao.problem.record.SubmittedProblemIdsRecord +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class BrowseUndoneProblemsUseCaseTest : BehaviorSpec({ + lateinit var problemDao: ProblemDao + lateinit var subscriptionService: SubscriptionService + lateinit var articleService: ArticleService + lateinit var submitHistoryDao: SubmitHistoryDao + lateinit var useCase: BrowseUndoneProblemsUseCase + + beforeContainer { + problemDao = mockk() + subscriptionService = mockk() + articleService = mockk() + submitHistoryDao = mockk() + useCase = BrowseUndoneProblemsUseCase(problemDao, subscriptionService, articleService, submitHistoryDao) + } + + given("밀린 문제 ID 조회 요청이 온 상황에서") { + val memberId = 0L + val useCaseIn = BrowseUndoneProblemsUseCaseIn(memberId = memberId) + + `when`("밀린 문제가 존재할 경우") { + every { subscriptionService.browseWorkbookIdAndProgress(any()) } returns listOf( + SubscriptionProgressOutDto(1L, 3), + SubscriptionProgressOutDto(2L, 3), + SubscriptionProgressOutDto(3L, 5), + SubscriptionProgressOutDto(4L, 7) + ) + + every { articleService.browseArticleIdByWorkbookIdLimitDay(any()) } returns listOf(1L, 2L) + + every { problemDao.selectProblemIdByArticleIds(any()) } returns listOf( + ProblemIdAndArticleIdRecord(1L, 2L), + ProblemIdAndArticleIdRecord(2L, 2L) + ) + + every { submitHistoryDao.selectProblemIdByProblemIds(any()) } returns SubmittedProblemIdsRecord( + listOf( + 1L, + 2L + ) + ) + + then("밀린 문제 ID 목록을 반환한다") { + useCase.execute(useCaseIn) + + verify(exactly = 1) { subscriptionService.browseWorkbookIdAndProgress(any()) } + verify(exactly = 4) { articleService.browseArticleIdByWorkbookIdLimitDay(any()) } + verify(exactly = 1) { problemDao.selectProblemIdByArticleIds(any()) } + verify(exactly = 1) { submitHistoryDao.selectProblemIdByProblemIds(any()) } + } + } + + `when`("구독중이 워크북이 없을 경우") { + every { subscriptionService.browseWorkbookIdAndProgress(any()) } returns emptyList() + + every { articleService.browseArticleIdByWorkbookIdLimitDay(any()) } returns listOf(1L, 2L) + + every { problemDao.selectProblemIdByArticleIds(any()) } returns listOf( + ProblemIdAndArticleIdRecord(1L, 2L), + ProblemIdAndArticleIdRecord(2L, 2L) + ) + + every { submitHistoryDao.selectProblemIdByProblemIds(any()) } returns SubmittedProblemIdsRecord( + listOf( + 1L, + 2L + ) + ) + + then("에러를 반환한다") { + shouldThrow { useCase.execute(useCaseIn) } + + verify(exactly = 1) { subscriptionService.browseWorkbookIdAndProgress(any()) } + verify(exactly = 0) { articleService.browseArticleIdByWorkbookIdLimitDay(any()) } + verify(exactly = 0) { problemDao.selectProblemIdByArticleIds(any()) } + verify(exactly = 0) { submitHistoryDao.selectProblemIdByProblemIds(any()) } + } + } + } +}) \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt index ff4e7d8e9..79acdfdf6 100644 --- a/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt @@ -33,7 +33,8 @@ class ReadProblemUseCaseTest : BehaviorSpec({ `when`("문제가 존재할 경우") { val title = "title" val problemContents = "{}" - every { problemDao.selectProblemContents(any()) } returns SelectProblemRecord(id = problemId, title = title, contents = problemContents) + val articleId = 3L + every { problemDao.selectProblemContents(any()) } returns SelectProblemRecord(id = problemId, title = title, contents = problemContents, articleId = articleId) val contentCount = 2 every { contentsJsonMapper.toObject(any()) } returns Contents( diff --git a/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt b/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt index 7a1603e49..a416af423 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt @@ -9,6 +9,7 @@ import com.few.api.domain.log.AddApiLogUseCase import com.few.api.domain.member.usecase.SaveMemberUseCase import com.few.api.domain.member.usecase.TokenUseCase import com.few.api.domain.problem.usecase.BrowseProblemsUseCase +import com.few.api.domain.problem.usecase.BrowseUndoneProblemsUseCase import com.few.api.domain.problem.usecase.CheckProblemUseCase import com.few.api.domain.problem.usecase.ReadProblemUseCase import com.few.api.domain.subscription.usecase.BrowseSubscribeWorkbooksUseCase @@ -92,6 +93,9 @@ abstract class ControllerTestSpec { @MockBean lateinit var checkProblemUseCase: CheckProblemUseCase + @MockBean + lateinit var browseUndoneProblemsUseCase: BrowseUndoneProblemsUseCase + /** SubscriptionControllerTest */ @MockBean lateinit var subscribeWorkbookUseCase: SubscribeWorkbookUseCase diff --git a/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt index dbfae1706..150f8bf1f 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt @@ -4,17 +4,11 @@ import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema +import com.few.api.domain.problem.usecase.dto.* import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* import com.few.api.web.controller.problem.request.CheckProblemRequest -import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseIn -import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseIn -import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseIn -import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseOut -import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseOut -import com.few.api.domain.problem.usecase.dto.ReadProblemContentsUseCaseOutDetail -import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseOut import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` @@ -73,7 +67,9 @@ class ProblemControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data") .fieldWithObject("data"), PayloadDocumentation.fieldWithPath("data.problemIds[]") - .fieldWithArray("문제 Id 목록") + .fieldWithArray("문제 Id 목록"), + PayloadDocumentation.fieldWithPath("data.size") + .fieldWithNumber("문제 갯수") ) ) ) @@ -94,6 +90,7 @@ class ProblemControllerTest : ControllerTestSpec() { .toUriString() val problemId = 1L + val articleId = 3L val useCaseIn = ReadProblemUseCaseIn(problemId) val useCaseOut = ReadProblemUseCaseOut( id = problemId, @@ -103,7 +100,8 @@ class ProblemControllerTest : ControllerTestSpec() { ReadProblemContentsUseCaseOutDetail(2L, "높은 운용 비용"), ReadProblemContentsUseCaseOutDetail(3L, "유동성"), ReadProblemContentsUseCaseOutDetail(4L, "투명성") - ) + ), + articleId = articleId ) `when`(readProblemUseCase.execute(useCaseIn)).thenReturn(useCaseOut) @@ -139,7 +137,9 @@ class ProblemControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.contents[].number") .fieldWithNumber("문제 선지 번호"), PayloadDocumentation.fieldWithPath("data.contents[].content") - .fieldWithString("문제 선지 내용") + .fieldWithString("문제 선지 내용"), + PayloadDocumentation.fieldWithPath("data.articleId") + .fieldWithNumber("문제가 속한 아티클 ID") ) ) ) @@ -208,4 +208,52 @@ class ProblemControllerTest : ControllerTestSpec() { ) ) } + + @Test + @DisplayName("[GET] /api/v1/problems/unsubmitted") + fun browseUndoneProblems() { + // given + val api = "BrowseUndoneProblems" + val memberId = 0L + val uri = UriComponentsBuilder.newInstance() + .path("$BASE_URL/unsubmitted").build().toUriString() + + val useCaseIn = BrowseUndoneProblemsUseCaseIn(memberId) + val useCaseOut = BrowseProblemsUseCaseOut(listOf(1L, 2L, 3L)) + `when`(browseUndoneProblemsUseCase.execute(useCaseIn)).thenReturn(useCaseOut) + + // when + mockMvc.perform( + get(uri) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( + api.toIdentifier(), + resource( + ResourceSnippetParameters.builder() + .description("밀린 문제 ID 목록 조회") + .summary(api.toIdentifier()) + .privateResource(false) + .deprecated(false) + .tag(TAG) + .requestSchema(Schema.schema(api.toRequestSchema())) + .responseSchema(Schema.schema(api.toResponseSchema())) + .responseFields( + *Description.describe( + arrayOf( + PayloadDocumentation.fieldWithPath("data") + .fieldWithObject("data"), + PayloadDocumentation.fieldWithPath("data.problemIds[]") + .fieldWithArray("밀린 문제 Id 목록"), + PayloadDocumentation.fieldWithPath("data.size") + .fieldWithNumber("밀린 문제 갯수") + ) + ) + ) + .build() + ) + ) + ) + } } \ No newline at end of file