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/#430] 밀린 문제 ID 조회 API 추가 #433

Merged
merged 15 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.article.query

data class SelectAritlceIdByWorkbookIdAndDayQuery(
val workbookId: Long,
val day: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.article.record

data class ArticleIdRecord(
val articleIds: List<Long>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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<ProblemIdAndArticleIdRecord> {
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)
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.problem.query

data class SelectProblemIdByArticleIdsQuery(
val articleIds: Set<Long>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.problem.query

data class SelectSubmittedProblemIdsQuery(
val memberId: Long,
val problemIds: List<Long>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.problem.record

data class ProblemIdAndArticleIdRecord(
val problemId: Long,
val articleId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ data class SelectProblemRecord(
val id: Long,
val title: String,
val contents: String,
val articleId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.repo.dao.problem.record

data class SubmittedProblemIdsRecord(
val problemIds: List<Long>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubscriptionProgressRecord> {
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.repo.dao.subscription.record

data class SubscriptionProgressRecord(
val workbookId: Long,
val day: Int,
)
Original file line number Diff line number Diff line change
@@ -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<Long> {
return articleDao.selectArticleIdsByWorkbookIdLimitDay(
SelectAritlceIdByWorkbookIdAndDayQuery(
inDto.workbookId,
inDto.day
)
).articleIds
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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.exception.common.NotFoundException
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<SubscriptionProgressOutDto> {
val subscriptionProgresses = subscriptionDao.selectWorkbookIdAndProgressByMember(
SelectSubscriptionSendStatusQuery(inDto.memberId)
).takeIf { it.isNotEmpty() } ?: throw NotFoundException("subscribe.workbook.notexist")

return subscriptionProgresses.map { SubscriptionProgressOutDto(it.workbookId, it.day) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.domain.problem.service.dto

data class BrowseArticleIdInDto(
val workbookId: Long,
val day: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.problem.service.dto

data class BrowseWorkbookIdAndProgressInDto(
val memberId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.few.api.domain.problem.service.dto

data class SubscriptionProgressOutDto(
val workbookId: Long,
val day: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.few.api.domain.problem.usecase
Copy link
Collaborator

Choose a reason for hiding this comment

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

BrowseUndoneProblemsUseCase는 problem이 맞을까요? 아님 member로 가는게 맞을까요?
제출하지 않은 문제 기록이니까 뭔가 member에 있는것도 고려할 수 있을 것 같아서요

Copy link
Member Author

Choose a reason for hiding this comment

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

이 API가 사용되는 그 resource 차원에서 볼때, problem에 더 가까울거 같습니다. 멤버를 기준으로 조회할 뿐 그 조회하려는 resource는 문제이기 때문

Copy link
Collaborator

Choose a reason for hiding this comment

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

지금 API가 ~/problems/unsubmitte로 되어 있는데
만약 해당 구현이 member로 간다면 ~/members/problems로 될 것 같아요

RESTful API 네이밍을 할때 명사를 사용하라는 추천이 있잖아요.
그 말을 고려하면 member로 괜찮지 않을까 생각해서 코멘트 남겼어요.

근데 problem에 있어도 괜찮을 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

명사를 사용하는게 룰이긴 한데 맨데토리는 아니고 동사가 필요할 경우앤 저 api 처럼 맨 뒤에만 위치하도록 해야 함

Copy link
Member Author

Choose a reason for hiding this comment

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

아 잠만 근데 저것보다 쿼리 파람으로 두는게 잴 적절하긴 하겠네요…


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.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)
)

/**
* 위에서 조회한 워크부에 속한 아티클 개수에 대해 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<Long, List<Long>> = 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()
Copy link
Collaborator

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.

지피티가 알려줌ㅋㅋ 내부적으론 어떤 성능 이슈나 그런게 있을지는 모르겠는데 파악해볼게요

val problemIdsRandomizedByArticleId = mutableListOf<Long>()

randomArticleIds.forEach { articleId ->
unsubmittedProblemIdAndArticleIds[articleId]?.let { problemIds ->
problemIdsRandomizedByArticleId.addAll(problemIds)
}
}

Comment on lines +47 to +79
Copy link
Member Author

Choose a reason for hiding this comment

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

articleId를 기준으로 랜덤화한 뒤 problemId를 뽑아냅니다

Copy link
Member Author

Choose a reason for hiding this comment

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

최대한 변수명이나 주석으로 표현은 해뒀는데 이해 안되시는 부분 말씀해주세요

Copy link
Collaborator

Choose a reason for hiding this comment

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

네 이해했슴다~

return BrowseProblemsUseCaseOut(problemIdsRandomizedByArticleId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ class ReadProblemUseCase(
return ReadProblemUseCaseOut(
id = record.id,
title = record.title,
contents = contents
contents = contents,
articleId = record.articleId
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.few.api.domain.problem.usecase.dto

data class BrowseUndoneProblemsUseCaseIn(
val memberId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class ReadProblemUseCaseOut(
val id: Long,
val title: String,
val contents: List<ReadProblemContentsUseCaseOutDetail>,
val articleId: Long,
)

data class ReadProblemContentsUseCaseOutDetail(
Expand Down
Loading
Loading