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 67899a04..fa7948c6 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 @@ -5,6 +5,7 @@ import com.few.api.repo.dao.subscription.query.* import com.few.api.repo.dao.subscription.record.WorkbookSubscriptionStatus import com.few.api.repo.dao.subscription.record.CountAllSubscriptionStatusRecord import com.few.api.repo.dao.subscription.record.MemberWorkbookSubscriptionStatusRecord +import com.few.api.repo.dao.subscription.record.SubscriptionSendStatusRecord import jooq.jooq_dsl.Tables.MAPPING_WORKBOOK_ARTICLE import jooq.jooq_dsl.Tables.SUBSCRIPTION import jooq.jooq_dsl.tables.MappingWorkbookArticle @@ -17,6 +18,21 @@ import java.time.LocalDateTime class SubscriptionDao( private val dslContext: DSLContext, ) { + fun getLock(memberId: Long, workbookId: Long, timeout: Int = 5): Boolean { + return dslContext.fetch( + """ + SELECT GET_LOCK(CONCAT('subscription_', $memberId, '_', $workbookId), $timeout); + """ + ).into(Int::class.java).first() == 1 + } + + fun releaseLock(memberId: Long, workbookId: Long): Boolean { + return dslContext.fetch( + """ + SELECT RELEASE_LOCK(CONCAT('subscription_', $memberId, '_', $workbookId)); + """ + ).into(Int::class.java).first() == 1 + } fun insertWorkbookSubscription(command: InsertWorkbookSubscriptionCommand) { insertWorkbookSubscriptionCommand(command) @@ -70,17 +86,19 @@ class SubscriptionDao( .orderBy(SUBSCRIPTION.CREATED_AT.desc()) .limit(1) - fun selectAllInActiveWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookInActiveSubscription): List { + fun selectAllInActiveWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookInActiveSubscriptionQuery): List { return selectAllWorkbookInActiveSubscriptionStatusQuery(query) .fetchInto(MemberWorkbookSubscriptionStatusRecord::class.java) } - fun selectAllWorkbookInActiveSubscriptionStatusQuery(query: SelectAllMemberWorkbookInActiveSubscription) = + fun selectAllWorkbookInActiveSubscriptionStatusQuery(query: SelectAllMemberWorkbookInActiveSubscriptionQuery) = dslContext.select( SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(MemberWorkbookSubscriptionStatusRecord::workbookId.name), SUBSCRIPTION.DELETED_AT.isNull.`as`(MemberWorkbookSubscriptionStatusRecord::isActiveSub.name), - DSL.max(SUBSCRIPTION.PROGRESS).add(1).`as`(MemberWorkbookSubscriptionStatusRecord::currentDay.name), - DSL.max(MAPPING_WORKBOOK_ARTICLE.DAY_COL).`as`(MemberWorkbookSubscriptionStatusRecord::totalDay.name) + DSL.max(SUBSCRIPTION.PROGRESS).add(1) + .`as`(MemberWorkbookSubscriptionStatusRecord::currentDay.name), + DSL.max(MAPPING_WORKBOOK_ARTICLE.DAY_COL) + .`as`(MemberWorkbookSubscriptionStatusRecord::totalDay.name) ) .from(SUBSCRIPTION) .join(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) @@ -89,14 +107,15 @@ class SubscriptionDao( .and(SUBSCRIPTION.TARGET_MEMBER_ID.isNull) .and(SUBSCRIPTION.UNSUBS_OPINION.eq(query.unsubOpinion)) .groupBy(SUBSCRIPTION.TARGET_WORKBOOK_ID, SUBSCRIPTION.DELETED_AT) + .orderBy(SUBSCRIPTION.PROGRESS) .query - fun selectAllActiveWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookActiveSubscription): List { + fun selectAllActiveWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookActiveSubscriptionQuery): List { return selectAllWorkbookActiveSubscriptionStatusQuery(query) .fetchInto(MemberWorkbookSubscriptionStatusRecord::class.java) } - fun selectAllWorkbookActiveSubscriptionStatusQuery(query: SelectAllMemberWorkbookActiveSubscription) = + fun selectAllWorkbookActiveSubscriptionStatusQuery(query: SelectAllMemberWorkbookActiveSubscriptionQuery) = dslContext.select( SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(MemberWorkbookSubscriptionStatusRecord::workbookId.name), SUBSCRIPTION.DELETED_AT.isNull.`as`(MemberWorkbookSubscriptionStatusRecord::isActiveSub.name), @@ -110,6 +129,7 @@ class SubscriptionDao( .and(SUBSCRIPTION.TARGET_MEMBER_ID.isNull) .and(SUBSCRIPTION.UNSUBS_OPINION.isNull) .groupBy(SUBSCRIPTION.TARGET_WORKBOOK_ID, SUBSCRIPTION.DELETED_AT) + .orderBy(SUBSCRIPTION.PROGRESS) .query fun updateDeletedAtInAllSubscription(command: UpdateDeletedAtInAllSubscriptionCommand) { @@ -148,7 +168,7 @@ class SubscriptionDao( * key: workbookId * value: workbook 구독 전체 기록 수 */ - fun countAllWorkbookSubscription(query: CountAllWorkbooksSubscription): Map { + fun countAllWorkbookSubscription(query: CountAllWorkbooksSubscriptionQuery): Map { return countAllWorkbookSubscriptionQuery() .fetch() .intoMap(SUBSCRIPTION.TARGET_WORKBOOK_ID, DSL.count(SUBSCRIPTION.TARGET_WORKBOOK_ID)) @@ -185,4 +205,57 @@ class SubscriptionDao( .set(SUBSCRIPTION.UNSUBS_OPINION, command.opinion) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) + + fun selectAllSubscriptionSendStatus(query: SelectAllSubscriptionSendStatusQuery): List { + return selectAllSubscriptionSendStatusQuery(query) + .fetchInto( + SubscriptionSendStatusRecord::class.java + ) + } + + fun selectAllSubscriptionSendStatusQuery(query: SelectAllSubscriptionSendStatusQuery) = + dslContext.select( + SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(SubscriptionSendStatusRecord::workbookId.name), + SUBSCRIPTION.SEND_TIME.`as`(SubscriptionSendStatusRecord::sendTime.name), + SUBSCRIPTION.SEND_DAY.`as`(SubscriptionSendStatusRecord::sendDay.name) + ) + .from(SUBSCRIPTION) + .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) + .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.`in`(query.workbookIds)) + + fun selectAllActiveSubscriptionWorkbookIds(query: SelectAllActiveSubscriptionWorkbookIdsQuery): List { + return dslContext.select(SUBSCRIPTION.TARGET_WORKBOOK_ID) + .from(SUBSCRIPTION) + .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) + .and(SUBSCRIPTION.DELETED_AT.isNull) + .fetchInto(Long::class.java) + } + + data class SelectAllActiveSubscriptionWorkbookIdsQuery( + val memberId: Long, + ) + + fun bulkUpdateSubscriptionSendTime(command: BulkUpdateSubscriptionSendTimeCommand) { + bulkUpdateSubscriptionSendTimeCommand(command) + .execute() + } + + fun bulkUpdateSubscriptionSendTimeCommand(command: BulkUpdateSubscriptionSendTimeCommand) = + dslContext.update(SUBSCRIPTION) + .set(SUBSCRIPTION.SEND_TIME, command.time) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) + .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.`in`(command.workbookIds)) + + fun bulkUpdateSubscriptionSendDay(command: BulkUpdateSubscriptionSendDayCommand) { + bulkUpdateSubscriptionSendDayCommand(command) + .execute() + } + + fun bulkUpdateSubscriptionSendDayCommand(command: BulkUpdateSubscriptionSendDayCommand) = + dslContext.update(SUBSCRIPTION) + .set(SUBSCRIPTION.SEND_DAY, command.day) + .set(SUBSCRIPTION.MODIFIED_AT, LocalDateTime.now()) + .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) + .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.`in`(command.workbookIds)) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendDayCommand.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendDayCommand.kt new file mode 100644 index 00000000..2b0ed2ad --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendDayCommand.kt @@ -0,0 +1,7 @@ +package com.few.api.repo.dao.subscription.command + +data class BulkUpdateSubscriptionSendDayCommand( + val memberId: Long, + val day: String, + val workbookIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendTimeCommand.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendTimeCommand.kt new file mode 100644 index 00000000..5e13d972 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/command/BulkUpdateSubscriptionSendTimeCommand.kt @@ -0,0 +1,9 @@ +package com.few.api.repo.dao.subscription.command + +import java.time.LocalTime + +data class BulkUpdateSubscriptionSendTimeCommand( + val memberId: Long, + val time: LocalTime, + val workbookIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscription.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscriptionQuery.kt similarity index 63% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscription.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscriptionQuery.kt index 1dcfa775..d87d6f92 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscription.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/CountAllWorkbooksSubscriptionQuery.kt @@ -1,5 +1,5 @@ package com.few.api.repo.dao.subscription.query -data class CountAllWorkbooksSubscription( +data class CountAllWorkbooksSubscriptionQuery( val workbookIds: List, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscription.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscriptionQuery.kt similarity index 71% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscription.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscriptionQuery.kt index 6a71f16f..f0d8eba9 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscription.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookActiveSubscriptionQuery.kt @@ -3,6 +3,6 @@ package com.few.api.repo.dao.subscription.query /** * 멤버의 구독 중인 워크북 목록을 조회합니다. */ -data class SelectAllMemberWorkbookActiveSubscription( +data class SelectAllMemberWorkbookActiveSubscriptionQuery( val memberId: Long, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscription.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscriptionQuery.kt similarity index 75% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscription.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscriptionQuery.kt index f1c02a08..d09bed59 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscription.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookInActiveSubscriptionQuery.kt @@ -3,7 +3,7 @@ package com.few.api.repo.dao.subscription.query /** * 멤버의 구독 완료 워크북 목록을 조회합니다. */ -data class SelectAllMemberWorkbookInActiveSubscription( +data class SelectAllMemberWorkbookInActiveSubscriptionQuery( val memberId: Long, val unsubOpinion: String = "receive.all", ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllSubscriptionSendStatusQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllSubscriptionSendStatusQuery.kt new file mode 100644 index 00000000..811924ec --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllSubscriptionSendStatusQuery.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.subscription.query + +data class SelectAllSubscriptionSendStatusQuery( + val memberId: Long, + val workbookIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionSendStatusRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionSendStatusRecord.kt new file mode 100644 index 00000000..048fa2a9 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/record/SubscriptionSendStatusRecord.kt @@ -0,0 +1,9 @@ +package com.few.api.repo.dao.subscription.record + +import java.time.LocalTime + +data class SubscriptionSendStatusRecord( + val workbookId: Long, + val sendTime: LocalTime, + val sendDay: String, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt index e18c6555..561a0e00 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt @@ -4,11 +4,13 @@ import com.few.api.repo.config.LocalCacheConfig import com.few.api.repo.config.LocalCacheConfig.Companion.LOCAL_CM import com.few.api.repo.dao.workbook.command.InsertWorkBookCommand import com.few.api.repo.dao.workbook.command.MapWorkBookToArticleCommand -import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCount +import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCountQuery +import com.few.api.repo.dao.workbook.query.SelectAllWorkbookTitleQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookLastArticleIdQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery import com.few.api.repo.dao.workbook.record.SelectWorkBookRecord import com.few.api.repo.dao.workbook.record.SelectWorkBookRecordWithSubscriptionCount +import com.few.api.repo.dao.workbook.record.WorkbookTitleRecord import com.few.data.common.code.CategoryType import jooq.jooq_dsl.tables.MappingWorkbookArticle import jooq.jooq_dsl.tables.Subscription @@ -71,12 +73,12 @@ class WorkbookDao( * category에 따라서 조회된 구독자 수가 포함된 Workbook 목록을 반환한다. * 정렬 순서는 구독자 수가 많은 순서로, 구독자 수가 같다면 생성일자가 최신인 순서로 반환한다. */ - fun browseWorkBookWithSubscriptionCount(query: BrowseWorkBookQueryWithSubscriptionCount): List { + fun browseWorkBookWithSubscriptionCount(query: BrowseWorkBookQueryWithSubscriptionCountQuery): List { return browseWorkBookQuery(query) .fetchInto(SelectWorkBookRecordWithSubscriptionCount::class.java) } - fun browseWorkBookQuery(query: BrowseWorkBookQueryWithSubscriptionCount) = + fun browseWorkBookQuery(query: BrowseWorkBookQueryWithSubscriptionCountQuery) = dslContext.select( Workbook.WORKBOOK.ID.`as`(SelectWorkBookRecordWithSubscriptionCount::id.name), Workbook.WORKBOOK.TITLE.`as`(SelectWorkBookRecordWithSubscriptionCount::title.name), @@ -124,7 +126,7 @@ class WorkbookDao( /** * category에 따라서 조건을 생성한다. */ - private fun browseWorkBookCategoryCondition(query: BrowseWorkBookQueryWithSubscriptionCount): Condition { + private fun browseWorkBookCategoryCondition(query: BrowseWorkBookQueryWithSubscriptionCountQuery): Condition { return when (query.category) { (-1).toByte() -> DSL.noCondition() else -> Workbook.WORKBOOK.CATEGORY_CD.eq(query.category) @@ -144,4 +146,17 @@ class WorkbookDao( .where(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(query.workbookId)) .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) .groupBy(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID) + + fun selectAllWorkbookTitle(query: SelectAllWorkbookTitleQuery): List { + return selectAllWorkbookTitleQuery(query) + .fetchInto(WorkbookTitleRecord::class.java) + } + + fun selectAllWorkbookTitleQuery(query: SelectAllWorkbookTitleQuery) = + dslContext.select( + Workbook.WORKBOOK.ID.`as`(WorkbookTitleRecord::workbookId.name), + Workbook.WORKBOOK.TITLE.`as`(WorkbookTitleRecord::title.name) + ) + .from(Workbook.WORKBOOK) + .where(Workbook.WORKBOOK.ID.`in`(query.workbookIds)) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCount.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCountQuery.kt similarity index 70% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCount.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCountQuery.kt index 854e8ffb..f5aa8e89 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCount.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/BrowseWorkBookQueryWithSubscriptionCountQuery.kt @@ -1,6 +1,6 @@ package com.few.api.repo.dao.workbook.query -data class BrowseWorkBookQueryWithSubscriptionCount( +data class BrowseWorkBookQueryWithSubscriptionCountQuery( /** * @see com.few.api.web.support.WorkBookCategory */ diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/SelectAllWorkbookTitleQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/SelectAllWorkbookTitleQuery.kt new file mode 100644 index 00000000..1bfa772d --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/query/SelectAllWorkbookTitleQuery.kt @@ -0,0 +1,5 @@ +package com.few.api.repo.dao.workbook.query + +data class SelectAllWorkbookTitleQuery( + val workbookIds: List, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/record/WorkbookTitleRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/record/WorkbookTitleRecord.kt new file mode 100644 index 00000000..f6531c5f --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/record/WorkbookTitleRecord.kt @@ -0,0 +1,6 @@ +package com.few.api.repo.dao.workbook.record + +data class WorkbookTitleRecord( + val workbookId: Long, + val title: String, +) \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt index f2142824..c918e0a4 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt @@ -2,10 +2,7 @@ package com.few.api.repo.explain.subscription import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.* -import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscription -import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSubscription +import com.few.api.repo.dao.subscription.query.* import com.few.api.repo.explain.ExplainGenerator import com.few.api.repo.explain.InsertUpdateExplainGenerator import com.few.api.repo.explain.ResultGenerator @@ -63,7 +60,7 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { @Test fun selectAllWorkbookInActiveSubscriptionStatusQueryExplain() { - val query = SelectAllMemberWorkbookInActiveSubscription( + val query = SelectAllMemberWorkbookInActiveSubscriptionQuery( memberId = 1L, unsubOpinion = "receive.all" ).let { @@ -77,7 +74,7 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { @Test fun selectAllWorkbookActiveSubscriptionStatusQueryExplain() { - val query = SelectAllMemberWorkbookActiveSubscription( + val query = SelectAllMemberWorkbookActiveSubscriptionQuery( memberId = 1L ).let { subscriptionDao.selectAllWorkbookActiveSubscriptionStatusQuery(it) @@ -190,4 +187,18 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(command, explain, "updateLastArticleProgressCommandExplain") } + + @Test + fun selectAllSubscriptionSendStatusQueryExplain() { + val query = subscriptionDao.selectAllSubscriptionSendStatusQuery( + SelectAllSubscriptionSendStatusQuery( + memberId = 1L, + workbookIds = listOf(1L) + ) + ) + + val explain = ExplainGenerator.execute(dslContext, query) + + ResultGenerator.execute(query, explain, "selectAllSubscriptionSendStatusQueryExplain") + } } \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt index 9406418b..db176769 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt @@ -3,7 +3,8 @@ package com.few.api.repo.explain.workbook import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.command.InsertWorkBookCommand import com.few.api.repo.dao.workbook.command.MapWorkBookToArticleCommand -import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCount +import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCountQuery +import com.few.api.repo.dao.workbook.query.SelectAllWorkbookTitleQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookLastArticleIdQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery import com.few.api.repo.explain.ExplainGenerator @@ -73,7 +74,7 @@ class WorkbookDaoExplainGenerateTest : JooqTestSpec() { @Test fun browseWorkBookQueryNoConditionQueryExplain() { - val query = BrowseWorkBookQueryWithSubscriptionCount(-1).let { + val query = BrowseWorkBookQueryWithSubscriptionCountQuery(-1).let { workbookDao.browseWorkBookQuery(it) } @@ -84,7 +85,7 @@ class WorkbookDaoExplainGenerateTest : JooqTestSpec() { @Test fun browseWorkBookQueryCategoryConditionExplain() { - val query = BrowseWorkBookQueryWithSubscriptionCount(CategoryType.fromCode(0)!!.code).let { + val query = BrowseWorkBookQueryWithSubscriptionCountQuery(CategoryType.fromCode(0)!!.code).let { workbookDao.browseWorkBookQuery(it) } @@ -118,4 +119,17 @@ class WorkbookDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "selectWorkBookLastArticleIdQueryExplain") } + + @Test + fun selectAllWorkbookTitleQueryExplain() { + val query = workbookDao.selectAllWorkbookTitleQuery( + SelectAllWorkbookTitleQuery( + listOf(1L) + ) + ) + + val explain = ExplainGenerator.execute(dslContext, query) + + ResultGenerator.execute(query, explain, "selectAllWorkbookTitleQueryExplain") + } } \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index ec683ab4..dc1735a1 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,7 +1,12 @@ import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI +import org.springframework.boot.gradle.tasks.bundling.BootJar import java.util.Random +tasks.withType(BootJar::class.java) { + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} + dependencies { /** module */ implementation(project(":api-repo")) @@ -13,12 +18,16 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-aop") /** jwt */ implementation("io.jsonwebtoken:jjwt-api:${DependencyVersion.JWT}") implementation("io.jsonwebtoken:jjwt-impl:${DependencyVersion.JWT}") implementation("io.jsonwebtoken:jjwt-jackson:${DependencyVersion.JWT}") + /** aspectj */ + implementation("org.aspectj:aspectjweaver:1.9.5") + /** scrimage */ implementation("com.sksamuel.scrimage:scrimage-core:${DependencyVersion.SCRIMAGE}") /** for convert to webp */ diff --git a/api/src/main/kotlin/com/few/api/client/repo/RepoClient.kt b/api/src/main/kotlin/com/few/api/client/repo/RepoClient.kt index 480b74d2..985da9fd 100644 --- a/api/src/main/kotlin/com/few/api/client/repo/RepoClient.kt +++ b/api/src/main/kotlin/com/few/api/client/repo/RepoClient.kt @@ -18,15 +18,15 @@ class RepoClient( private val log = KotlinLogging.logger {} fun announceRepoAlter(args: RepoAlterArgs) { - val embedsList = ArrayList() - args.let { - embedsList.add( - Embed( - title = "Exception", - description = "Slow Query Detected" - ) + val embedsList = mutableListOf( + Embed( + title = "Exception", + description = "Slow Query Detected" ) - it.requestURL.let { requestURL -> + ) + + args.let { arg -> + arg.requestURL.let { requestURL -> embedsList.add( Embed( title = "Request URL", @@ -34,7 +34,8 @@ class RepoClient( ) ) } - it.query?.let { query -> + + arg.query?.let { query -> embedsList.add( Embed( title = "Slow Query Detected", @@ -43,20 +44,19 @@ class RepoClient( ) } } - args.let { - DiscordBodyProperty( - content = "😭 DB 이상 발생", - embeds = embedsList - ) - }.let { body -> - restTemplate.exchange( - discordWebhook, - HttpMethod.POST, - HttpEntity(body), - String::class.java - ).let { res -> - log.info { "Discord webhook response: ${res.statusCode}" } - } + + restTemplate.exchange( + discordWebhook, + HttpMethod.POST, + HttpEntity( + DiscordBodyProperty( + content = "😭 DB 이상 발생", + embeds = embedsList + ) + ), + String::class.java + ).let { res -> + log.info { "Discord webhook response: ${res.statusCode}" } } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/client/repo/event/SlowQueryEventListener.kt b/api/src/main/kotlin/com/few/api/client/repo/event/SlowQueryEventListener.kt index 9cf42468..411acf11 100644 --- a/api/src/main/kotlin/com/few/api/client/repo/event/SlowQueryEventListener.kt +++ b/api/src/main/kotlin/com/few/api/client/repo/event/SlowQueryEventListener.kt @@ -17,11 +17,10 @@ class SlowQueryEventListener( @Async(value = DISCORD_HOOK_EVENT_POOL) @EventListener fun handleSlowQueryEvent(event: SlowQueryEvent) { - RepoAlterArgs( + val args = RepoAlterArgs( requestURL = MDC.get("Request-URL") ?: "", query = event.slowQuery - ).let { - repoClient.announceRepoAlter(it) - } + ) + repoClient.announceRepoAlter(args) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/client/subscription/SubscriptionClient.kt b/api/src/main/kotlin/com/few/api/client/subscription/SubscriptionClient.kt index a4bf3330..cf08ab5d 100644 --- a/api/src/main/kotlin/com/few/api/client/subscription/SubscriptionClient.kt +++ b/api/src/main/kotlin/com/few/api/client/subscription/SubscriptionClient.kt @@ -18,33 +18,33 @@ class SubscriptionClient( private val log = KotlinLogging.logger {} fun announceWorkbookSubscription(args: WorkbookSubscriptionArgs) { - args.let { + val body = args.let { arg -> DiscordBodyProperty( content = "🎉 신규 구독 알림 ", embeds = listOf( Embed( title = "Total Subscriptions", - description = it.totalSubscriptions.toString() + description = arg.totalSubscriptions.toString() ), Embed( title = "Active Subscriptions", - description = it.activeSubscriptions.toString() + description = arg.activeSubscriptions.toString() ), Embed( title = "Workbook Title", - description = it.workbookTitle + description = arg.workbookTitle ) ) ) - }.let { body -> - restTemplate.exchange( - discordWebhook, - HttpMethod.POST, - HttpEntity(body), - String::class.java - ).let { res -> - log.info { "Discord webhook response: ${res.statusCode}" } - } + } + + restTemplate.exchange( + discordWebhook, + HttpMethod.POST, + HttpEntity(body), + String::class.java + ).let { res -> + log.info { "Discord webhook response: ${res.statusCode}" } } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/config/ApiConfig.kt b/api/src/main/kotlin/com/few/api/config/ApiConfig.kt index 4f90acaa..cf96f3cf 100644 --- a/api/src/main/kotlin/com/few/api/config/ApiConfig.kt +++ b/api/src/main/kotlin/com/few/api/config/ApiConfig.kt @@ -9,8 +9,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import -import org.springframework.scheduling.annotation.EnableAsync -import org.springframework.web.servlet.config.annotation.EnableWebMvc @Configuration @ComponentScan(basePackages = [ApiConfig.BASE_PACKAGE]) @@ -21,8 +19,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc ImageStorageConfig::class, DocumentStorageConfig::class ) -@EnableWebMvc -@EnableAsync @ConfigurationPropertiesScan(basePackages = [ApiConfig.BASE_PACKAGE]) class ApiConfig { companion object { diff --git a/api/src/main/kotlin/com/few/api/config/AspectConfig.kt b/api/src/main/kotlin/com/few/api/config/AspectConfig.kt new file mode 100644 index 00000000..ad3a4e76 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/config/AspectConfig.kt @@ -0,0 +1,8 @@ +package com.few.api.config + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy + +@Configuration +@EnableAspectJAutoProxy +class AspectConfig \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/config/AsyncConfig.kt b/api/src/main/kotlin/com/few/api/config/AsyncConfig.kt new file mode 100644 index 00000000..c8833310 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/config/AsyncConfig.kt @@ -0,0 +1,8 @@ +package com.few.api.config + +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableAsync + +@Configuration +@EnableAsync +class AsyncConfig \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/config/crypto/IdEncryption.kt b/api/src/main/kotlin/com/few/api/config/crypto/IdEncryption.kt index 99fa78de..1827c631 100644 --- a/api/src/main/kotlin/com/few/api/config/crypto/IdEncryption.kt +++ b/api/src/main/kotlin/com/few/api/config/crypto/IdEncryption.kt @@ -16,19 +16,16 @@ class IdEncryption( @Value("\${security.encryption.iv}") private val iv: String, ) : Encryption { - private lateinit var key: SecretKeySpec - private lateinit var encodeCipher: Cipher - private lateinit var decodeCipher: Cipher - - init { - KeyGenerator.getInstance(algorithm).apply { - init(keySize) - key = SecretKeySpec(secretKey.toByteArray(), algorithm) - } - encodeCipher = Cipher.getInstance(transformation) - decodeCipher = Cipher.getInstance(transformation) - encodeCipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv.toByteArray())) - decodeCipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv.toByteArray())) + private var key: SecretKeySpec = KeyGenerator.getInstance(algorithm).apply { + init(keySize) + }.run { + SecretKeySpec(secretKey.toByteArray(), this@IdEncryption.algorithm) + } + private var encodeCipher: Cipher = Cipher.getInstance(transformation).apply { + init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(this@IdEncryption.iv.toByteArray())) + } + private var decodeCipher: Cipher = Cipher.getInstance(transformation).apply { + init(Cipher.DECRYPT_MODE, key, IvParameterSpec(this@IdEncryption.iv.toByteArray())) } override fun encrypt(plainText: String): String { diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt index 30d6c28a..a1fb700c 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt @@ -42,12 +42,12 @@ class ArticleMainCardService( val articleMainCardRecord: ArticleMainCardRecord = articleMainCardDao.selectArticleMainCardsRecord(setOf(inDto.articleId)) - .ifEmpty { throw NotFoundException("articlemaincard.notfound.id") } - .first() + .firstOrNull() ?: throw NotFoundException("article.notfound.id") val workbookCommands = - articleMainCardRecord.workbooks.map { WorkbookCommand(it.id!!, it.title!!) }.toMutableList() - workbookCommands.add(toBeAddedWorkbook) + articleMainCardRecord.workbooks.map { WorkbookCommand(it.id!!, it.title!!) } + .toMutableList() + .apply { add(toBeAddedWorkbook) } articleMainCardDao.updateArticleMainCardSetWorkbook( UpdateArticleMainCardWorkbookCommand( diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/service/GetUrlService.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/service/GetUrlService.kt index ff755c03..0479c3a9 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/service/GetUrlService.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/GetUrlService.kt @@ -21,13 +21,9 @@ class GetLocalUrlService( private val services: Map, ) : GetUrlService { override fun execute(query: GetUrlInDto): GetUrlOutDto { - val service = services.keys.stream().filter { key -> - key.lowercase(Locale.getDefault()) - .contains(query.getPreSignedUrlServiceKey()) - }.findAny().let { - if (it.isEmpty) throw IllegalArgumentException("Cannot find service for ${query.getPreSignedUrlServiceKey()}") - services[it.get()]!! - } + val service = services.keys.firstOrNull { key -> + key.lowercase(Locale.getDefault()).contains(query.getPreSignedUrlServiceKey()) + }?.let { services[it] } ?: throw IllegalArgumentException("Cannot find service for ${query.getPreSignedUrlServiceKey()}") return service.execute(query.`object`)?.let { GetUrlOutDto(URL(it)) diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddArticleUseCase.kt index 5218dfd0..1269e9b0 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddArticleUseCase.kt @@ -44,94 +44,64 @@ class AddArticleUseCase( ) { @Transactional fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut { - /** select writerId */ - val writerRecord = SelectMemberByEmailQuery(useCaseIn.writerEmail).let { - memberDao.selectMemberByEmail(it) - } ?: throw NotFoundException("member.notfound.id") + val writerRecord = memberDao.selectMemberByEmail(SelectMemberByEmailQuery(useCaseIn.writerEmail)) + ?: throw NotFoundException("member.notfound.id") - /** - * - content type: "md" - * put origin document to object storage - * and convert to html source - * - content type: "html" - * save html source - */ - val htmlSource = when { - useCaseIn.contentType.lowercase(Locale.getDefault()) == "md" -> { + val htmlSource = when (useCaseIn.contentType.lowercase(Locale.getDefault())) { + "md" -> { val mdSource = useCaseIn.contentSource - val htmlSource = convertDocumentService.mdToHtml(useCaseIn.contentSource) + val htmlSource = convertDocumentService.mdToHtml(mdSource) - val document = runCatching { - File.createTempFile("temp", ".md") - }.onSuccess { - it.writeText(mdSource) - }.getOrThrow() + val document = File.createTempFile("temp", ".md").apply { + writeText(mdSource) + } val documentName = ObjectPathGenerator.documentPath("md") - putDocumentService.execute(documentName, document)?.let { res -> + val url = putDocumentService.execute(documentName, document)?.let { res -> val source = res.`object` - GetUrlInDto(source).let { query -> - getUrlService.execute(query) - }.let { dto -> - InsertDocumentIfoCommand( - path = documentName, - url = dto.url - ).let { command -> - documentDao.insertDocumentIfo(command) - } - dto.url - } - } ?: throw ExternalIntegrationException("external.putfail.docummet") + getUrlService.execute(GetUrlInDto(source)).url + } ?: throw ExternalIntegrationException("external.putfail.document") + documentDao.insertDocumentIfo(InsertDocumentIfoCommand(path = documentName, url = url)) htmlSource } - - useCaseIn.contentType.lowercase(Locale.getDefault()) == "html" -> { - useCaseIn.contentSource - } - - else -> { - throw IllegalArgumentException("Unsupported content type: ${useCaseIn.contentType}") - } + "html" -> useCaseIn.contentSource + else -> throw IllegalArgumentException("Unsupported content type: ${useCaseIn.contentType}") } val category = CategoryType.fromName(useCaseIn.category) ?: throw NotFoundException("article.invalid.category") - /** insert article */ - val articleMstId = InsertFullArticleRecordCommand( - writerId = writerRecord.memberId, - mainImageURL = useCaseIn.articleImageUrl, - title = useCaseIn.title, - category = category.code, - content = htmlSource - ).let { articleDao.insertFullArticleRecord(it) } + val articleMstId = articleDao.insertFullArticleRecord( + InsertFullArticleRecordCommand( + writerId = writerRecord.memberId, + mainImageURL = useCaseIn.articleImageUrl, + title = useCaseIn.title, + category = category.code, + content = htmlSource + ) + ) - /** insert problems */ - useCaseIn.problems.stream().map { problemDatum -> + useCaseIn.problems.map { problemDatum -> InsertProblemsCommand( articleId = articleMstId, createrId = 0L, // todo fix title = problemDatum.title, contents = Contents( problemDatum.contents.map { detail -> - Content( - detail.number, - detail.content - ) + Content(detail.number, detail.content) } ), answer = problemDatum.answer, explanation = problemDatum.explanation ) - }.toList().let { commands -> + }.also { commands -> problemDao.insertProblems(commands) } - ArticleViewCountQuery( - articleMstId, - category - ).let { articleViewCountDao.insertArticleViewCountToZero(it) } + articleViewCountDao.insertArticleViewCountToZero( + ArticleViewCountQuery(articleMstId, category) + ) articleMainCardService.initialize( InitializeArticleMainCardInDto( diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddWorkbookUseCase.kt index 3bf30167..de264d2b 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/AddWorkbookUseCase.kt @@ -14,14 +14,15 @@ class AddWorkbookUseCase( ) { @Transactional fun execute(useCaseIn: AddWorkbookUseCaseIn): AddWorkbookUseCaseOut { - val workbookId = InsertWorkBookCommand( - title = useCaseIn.title, - mainImageUrl = useCaseIn.mainImageUrl, - category = useCaseIn.category, - description = useCaseIn.description - ).let { - workbookDao.insertWorkBook(it) - } ?: throw InsertException("workbook.insertfail.record") + val workbookId = + workbookDao.insertWorkBook( + InsertWorkBookCommand( + title = useCaseIn.title, + mainImageUrl = useCaseIn.mainImageUrl, + category = useCaseIn.category, + description = useCaseIn.description + ) + ) ?: throw InsertException("workbook.insertfail.record") return AddWorkbookUseCaseOut(workbookId) } diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/ConvertContentUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/ConvertContentUseCase.kt index 00e99ad9..30e8a5a0 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/ConvertContentUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/ConvertContentUseCase.kt @@ -7,7 +7,6 @@ import com.few.api.domain.admin.document.service.dto.GetUrlInDto import com.few.api.domain.admin.document.utils.ObjectPathGenerator import com.few.api.exception.common.ExternalIntegrationException import com.few.api.exception.common.InsertException - import com.few.api.repo.dao.document.DocumentDao import com.few.api.repo.dao.document.command.InsertDocumentIfoCommand import com.few.storage.document.service.ConvertDocumentService @@ -28,30 +27,30 @@ class ConvertContentUseCase( val contentSource = useCaseIn.content val documentSuffix = contentSource.originalFilename?.substringAfterLast(".") ?: "md" - val document = runCatching { - File.createTempFile("temp", ".$documentSuffix") - }.onSuccess { - contentSource.transferTo(it) - }.getOrThrow() + val document = File.createTempFile("temp", ".$documentSuffix").apply { + contentSource.transferTo(this) + } + val documentName = ObjectPathGenerator.documentPath(documentSuffix) - val originDownloadUrl = putDocumentService.execute(documentName, document)?.let { res -> - val source = res.`object` - GetUrlInDto(source).let { query -> - getUrlService.execute(query) - }.let { dto -> - InsertDocumentIfoCommand( - path = documentName, - url = dto.url - ).let { command -> - documentDao.insertDocumentIfo(command) ?: throw InsertException("document.insertfail.record") - } - dto.url - } - } ?: throw ExternalIntegrationException("external.document.presignedfail") + val originDownloadUrl = + putDocumentService.execute(documentName, document) + ?.`object` + ?.let { source -> + getUrlService.execute(GetUrlInDto(source)).also { dto -> + documentDao.insertDocumentIfo( + InsertDocumentIfoCommand( + path = documentName, + url = dto.url + ) + ) ?: throw InsertException("document.insertfail.record") + } + .let { savedDocument -> + savedDocument.url + } + } ?: throw ExternalIntegrationException("external.document.presignedfail") - val html = - convertDocumentService.mdToHtml(document.readBytes().toString(Charsets.UTF_8)) + val html = convertDocumentService.mdToHtml(document.readBytes().toString(Charsets.UTF_8)) return ConvertContentUseCaseOut(html.replace("\n", "
"), originDownloadUrl) } diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/MapArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/MapArticleUseCase.kt index 0cfefb7d..c2d3e4e0 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/MapArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/MapArticleUseCase.kt @@ -15,9 +15,14 @@ class MapArticleUseCase( ) { @Transactional fun execute(useCaseIn: MapArticleUseCaseIn) { - MapWorkBookToArticleCommand(useCaseIn.workbookId, useCaseIn.articleId, useCaseIn.dayCol).let { command -> - workbookDao.mapWorkBookToArticle(command) - } + workbookDao.mapWorkBookToArticle( + MapWorkBookToArticleCommand( + useCaseIn.workbookId, + useCaseIn.articleId, + useCaseIn.dayCol + ) + + ) articleMainCardService.appendWorkbook( AppendWorkbookToArticleMainCardInDto(useCaseIn.articleId, useCaseIn.workbookId) diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt index 2a066cdf..924e0541 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt @@ -29,41 +29,37 @@ class PutImageUseCase( val suffix = imageSource.originalFilename?.substringAfterLast(".") ?: "jpg" val imageName = ObjectPathGenerator.imagePath(suffix) - val originImage = runCatching { - File.createTempFile("temp", ".$suffix") - }.onSuccess { - imageSource.transferTo(it) - }.getOrThrow() + val originImage = File.createTempFile("temp", ".$suffix").apply { + imageSource.transferTo(this) + } val webpImage = ImmutableImage.loader().fromFile(originImage) .output(WebpWriter.DEFAULT, File.createTempFile("temp", ".webp")) - val url = putImageService.execute(imageName, originImage)?.let { res -> - val source = res.`object` - GetUrlInDto(source).let { query -> - getUrlService.execute(query) - }.let { dto -> - InsertImageIfoCommand(source, dto.url).let { command -> - imageDao.insertImageIfo(command) ?: throw InsertException("image.insertfail.record") + val url = putImageService.execute(imageName, originImage) + ?.`object` + ?.let { source -> + getUrlService.execute(GetUrlInDto(source)).also { dto -> + imageDao.insertImageIfo(InsertImageIfoCommand(source, dto.url)) + ?: throw InsertException("image.insertfail.record") } - return@let dto.url - } - } ?: throw ExternalIntegrationException("external.presignedfail.image") - - val webpUrl = - putImageService.execute(imageName.replaceAfterLast(".", "webp"), webpImage)?.let { res -> - val source = res.`object` - GetUrlInDto(source).let { query -> - getUrlService.execute(query) - }.let { dto -> - InsertImageIfoCommand(source, dto.url).let { command -> - imageDao.insertImageIfo(command) ?: throw InsertException("image.insertfail.record") + .let { savedImage -> + savedImage.url } - return@let dto.url + } ?: throw ExternalIntegrationException("external.presignedfail.image") + + val webpUrl = putImageService.execute(imageName, webpImage) + ?.`object` + ?.let { source -> + getUrlService.execute(GetUrlInDto(source)).also { dto -> + imageDao.insertImageIfo(InsertImageIfoCommand(source, dto.url)) + ?: throw InsertException("image.insertfail.record") } + .let { savedImage -> + savedImage.url + } } ?: throw ExternalIntegrationException("external.presignedfail.image") - // todo fix if webp is default return PutImageUseCaseOut(url, listOf(suffix, "webp")) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/BrowseArticleProblemsService.kt b/api/src/main/kotlin/com/few/api/domain/article/service/BrowseArticleProblemsService.kt index 113fc6e3..6aa313ec 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/service/BrowseArticleProblemsService.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/service/BrowseArticleProblemsService.kt @@ -7,15 +7,15 @@ import com.few.api.repo.dao.problem.ProblemDao import com.few.api.repo.dao.problem.query.SelectProblemsByArticleIdQuery import org.springframework.stereotype.Service +@Suppress("NAME_SHADOWING") @Service class BrowseArticleProblemsService( private val problemDao: ProblemDao, ) { fun execute(query: BrowseArticleProblemIdsInDto): BrowseArticleProblemsOutDto { - SelectProblemsByArticleIdQuery(query.articleId).let { query: SelectProblemsByArticleIdQuery -> - return problemDao.selectProblemsByArticleId(query)?.let { BrowseArticleProblemsOutDto(it.problemIds) } - ?: throw NotFoundException("problem.notfound.articleId") - } + return problemDao.selectProblemsByArticleId(SelectProblemsByArticleIdQuery(query.articleId)) + ?.let { BrowseArticleProblemsOutDto(it.problemIds) } + ?: throw NotFoundException("problem.notfound.articleId") } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt b/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt index ff010ad2..150a02d3 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt @@ -6,15 +6,15 @@ import com.few.api.repo.dao.member.MemberDao import com.few.api.repo.dao.member.query.SelectWriterQuery import org.springframework.stereotype.Service +@Suppress("NAME_SHADOWING") @Service class ReadArticleWriterRecordService( private val memberDao: MemberDao, ) { fun execute(query: ReadWriterRecordInDto): ReadWriterOutDto? { - SelectWriterQuery(query.writerId).let { query: SelectWriterQuery -> - val record = memberDao.selectWriter(query) - return record?.let { + return memberDao.selectWriter(SelectWriterQuery(query.writerId)) + ?.let { ReadWriterOutDto( writerId = it.writerId, name = it.name, @@ -22,6 +22,5 @@ class ReadArticleWriterRecordService( imageUrl = it.imageUrl ) } - } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt index 777e83be..ef56bb00 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/BrowseArticlesUseCase.kt @@ -60,7 +60,7 @@ class BrowseArticlesUseCase( /** * ARTICLE_MAIN_CARD 테이블에서 이번 스크롤에서 보여줄 10개 아티클 조회 (TODO: 캐싱 적용) */ - var articleMainCardRecords: Set = + val articleMainCardRecords: Set = articleMainCardDao.selectArticleMainCardsRecord(articleViewsRecords.map { it.articleId }.toSet()) /** diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt index d967e4f1..6753f169 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCase.kt @@ -28,18 +28,16 @@ class ReadArticleUseCase( @Transactional(readOnly = true) fun execute(useCaseIn: ReadArticleUseCaseIn): ReadArticleUseCaseOut { - val articleRecord = SelectArticleRecordQuery(useCaseIn.articleId).let { query: SelectArticleRecordQuery -> - articleDao.selectArticleRecord(query) - } ?: throw NotFoundException("article.notfound.id") + val articleRecord = + articleDao.selectArticleRecord(SelectArticleRecordQuery(useCaseIn.articleId)) + ?: throw NotFoundException("article.notfound.id") - val writerRecord = ReadWriterRecordInDto(articleRecord.writerId).let { query: ReadWriterRecordInDto -> - readArticleWriterRecordService.execute(query) ?: throw NotFoundException("writer.notfound.id") - } + val writerRecord = + readArticleWriterRecordService.execute(ReadWriterRecordInDto(articleRecord.writerId)) + ?: throw NotFoundException("writer.notfound.id") val problemIds = - BrowseArticleProblemIdsInDto(articleRecord.articleId).let { query: BrowseArticleProblemIdsInDto -> - browseArticleProblemsService.execute(query) - } + browseArticleProblemsService.execute(BrowseArticleProblemIdsInDto(articleRecord.articleId)) val views = articleViewCountHandler.browseArticleViewCount(useCaseIn.articleId) applicationEventPublisher.publishEvent( diff --git a/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt b/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt new file mode 100644 index 00000000..6ceb5015 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/common/lock/LockAspect.kt @@ -0,0 +1,77 @@ +package com.few.api.domain.common.lock + +import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookUseCaseIn +import com.few.api.repo.dao.subscription.SubscriptionDao +import io.github.oshai.kotlinlogging.KotlinLogging +import org.aspectj.lang.annotation.AfterReturning +import org.aspectj.lang.annotation.AfterThrowing +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.aspectj.lang.annotation.Pointcut +import org.aspectj.lang.reflect.MethodSignature +import org.springframework.stereotype.Component + +@Aspect +@Component +class LockAspect( + private val subscriptionDao: SubscriptionDao, +) { + private val log = KotlinLogging.logger {} + + @Pointcut("@annotation(com.few.api.domain.common.lock.LockFor)") + fun lockPointcut() {} + + @Before("lockPointcut()") + fun before(joinPoint: JoinPoint) { + getLockFor(joinPoint).run { + when (this.identifier) { + LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { + val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn + getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + } + } + } + } + + private fun getSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) { + subscriptionDao.getLock(useCaseIn.memberId, useCaseIn.workbookId).run { + if (!this) { + throw IllegalStateException("Already in progress for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}") + } + log.debug { "Lock acquired for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" } + } + } + + @AfterReturning("lockPointcut()") + fun afterReturning(joinPoint: JoinPoint) { + getLockFor(joinPoint).run { + when (this.identifier) { + LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { + val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn + releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + } + } + } + } + + @AfterThrowing("lockPointcut()") + fun afterThrowing(joinPoint: JoinPoint) { + getLockFor(joinPoint).run { + when (this.identifier) { + LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID -> { + val useCaseIn = joinPoint.args[0] as SubscribeWorkbookUseCaseIn + releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn) + } + } + } + } + + private fun getLockFor(joinPoint: JoinPoint) = + (joinPoint.signature as MethodSignature).method.getAnnotation(LockFor::class.java) + + private fun releaseSubscriptionMemberIdAndWorkBookIdLock(useCaseIn: SubscribeWorkbookUseCaseIn) { + subscriptionDao.releaseLock(useCaseIn.memberId, useCaseIn.workbookId) + log.debug { "Lock released for ${useCaseIn.memberId}'s subscription to ${useCaseIn.workbookId}" } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/common/lock/LockFor.kt b/api/src/main/kotlin/com/few/api/domain/common/lock/LockFor.kt new file mode 100644 index 00000000..bd1af418 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/common/lock/LockFor.kt @@ -0,0 +1,7 @@ +package com.few.api.domain.common.lock + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class LockFor( + val identifier: LockIdentifier, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/common/lock/LockIdentifier.kt b/api/src/main/kotlin/com/few/api/domain/common/lock/LockIdentifier.kt new file mode 100644 index 00000000..532e8089 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/common/lock/LockIdentifier.kt @@ -0,0 +1,8 @@ +package com.few.api.domain.common.lock + +enum class LockIdentifier { + /** + * 구독 테이블에 멤버와 워크북을 기준으로 락을 건다. + */ + SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID, +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/log/AddApiLogUseCase.kt b/api/src/main/kotlin/com/few/api/domain/log/AddApiLogUseCase.kt index 38e3dada..90ad5116 100644 --- a/api/src/main/kotlin/com/few/api/domain/log/AddApiLogUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/log/AddApiLogUseCase.kt @@ -12,8 +12,6 @@ class AddApiLogUseCase( ) { @Transactional fun execute(useCaseIn: AddApiLogUseCaseIn) { - InsertLogCommand(useCaseIn.history).let { - logIfoDao.insertLogIfo(it) - } + logIfoDao.insertLogIfo(InsertLogCommand(useCaseIn.history)) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/member/subscription/MemberSubscriptionService.kt b/api/src/main/kotlin/com/few/api/domain/member/subscription/MemberSubscriptionService.kt new file mode 100644 index 00000000..ac83fbfd --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/member/subscription/MemberSubscriptionService.kt @@ -0,0 +1,23 @@ +package com.few.api.domain.member.subscription + +import com.few.api.domain.member.subscription.dto.DeleteSubscriptionDto +import com.few.api.repo.dao.subscription.SubscriptionDao +import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInAllSubscriptionCommand +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class MemberSubscriptionService( + private val subscriptionDao: SubscriptionDao, +) { + + @Transactional + fun deleteSubscription(dto: DeleteSubscriptionDto) { + subscriptionDao.updateDeletedAtInAllSubscription( + UpdateDeletedAtInAllSubscriptionCommand( + memberId = dto.memberId, + opinion = dto.opinion + ) + ) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/member/subscription/dto/DeleteSubscriptionDto.kt b/api/src/main/kotlin/com/few/api/domain/member/subscription/dto/DeleteSubscriptionDto.kt new file mode 100644 index 00000000..dac23580 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/member/subscription/dto/DeleteSubscriptionDto.kt @@ -0,0 +1,6 @@ +package com.few.api.domain.member.subscription.dto + +data class DeleteSubscriptionDto( + val memberId: Long, + val opinion: String = "withdrawal", +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt index cb10ce4d..6a82ab1f 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/DeleteMemberUseCase.kt @@ -1,25 +1,33 @@ package com.few.api.domain.member.usecase +import com.few.api.domain.member.subscription.MemberSubscriptionService +import com.few.api.domain.member.subscription.dto.DeleteSubscriptionDto import com.few.api.domain.member.usecase.dto.DeleteMemberUseCaseIn import com.few.api.domain.member.usecase.dto.DeleteMemberUseCaseOut import com.few.api.repo.dao.member.MemberDao import com.few.api.repo.dao.member.command.DeleteMemberCommand import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional -import java.util.* @Component class DeleteMemberUseCase( private val memberDao: MemberDao, + private val memberSubscriptionService: MemberSubscriptionService, ) { @Transactional fun execute(useCaseIn: DeleteMemberUseCaseIn): DeleteMemberUseCaseOut { - DeleteMemberCommand( - memberId = useCaseIn.memberId - ).let { command -> - memberDao.deleteMember(command) - } + memberDao.deleteMember( + DeleteMemberCommand( + memberId = useCaseIn.memberId + ) + ) + memberSubscriptionService.deleteSubscription( + DeleteSubscriptionDto( + memberId = useCaseIn.memberId, + opinion = "cancel" + ) + ) return DeleteMemberUseCaseOut(true) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt index 30eddbb2..829aff52 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt @@ -48,29 +48,30 @@ class SaveMemberUseCase( headComment = SIGNUP_HEAD_COMMENT subComment = SIGNUP_SUB_COMMENT email = useCaseIn.email - InsertMemberCommand( - email = useCaseIn.email, - memberType = MemberType.PREAUTH - ).let { - memberDao.insertMember(it) ?: throw InsertException("member.insertfail.record") - } + memberDao.insertMember( + InsertMemberCommand( + email = useCaseIn.email, + memberType = MemberType.PREAUTH + ) + ) ?: throw InsertException("member.insertfail.record") } else { /** 삭제한 회원이라면 회원 타입을 PREAUTH로 변경 */ if (isSignUpBeforeMember!!.isDeleted) { - UpdateDeletedMemberTypeCommand( - id = isSignUpBeforeMember.memberId, - memberType = MemberType.PREAUTH - ).let { - val isUpdate = memberDao.updateMemberType(it) - if (isUpdate != 1L) { - throw InsertException("member.updatefail.record") - } - memberDao.selectMemberByEmail( - SelectMemberByEmailNotConsiderDeletedAtQuery( - email = useCaseIn.email - ) - )?.memberId ?: throw InsertException("member.selectfail.record") + val isUpdate = memberDao.updateMemberType( + UpdateDeletedMemberTypeCommand( + id = isSignUpBeforeMember.memberId, + memberType = MemberType.PREAUTH + ) + ) + if (isUpdate != 1L) { + throw InsertException("member.updatefail.record") } + + memberDao.selectMemberByEmail( + SelectMemberByEmailNotConsiderDeletedAtQuery( + email = useCaseIn.email + ) + )?.memberId ?: throw InsertException("member.selectfail.record") } else { /** 이미 가입한 회원이라면 회원 ID를 반환 */ isSignUpBeforeMember.memberId @@ -81,19 +82,19 @@ class SaveMemberUseCase( } runCatching { - SendAuthEmailArgs( - to = useCaseIn.email, - subject = "[FEW] 인증 이메일 주소를 확인해주세요.", - template = "auth", - content = Content( - headComment = headComment, - subComment = subComment, - email = email, - confirmLink = URL("https://www.fewletter.com/auth/validation/complete?auth_token=$token") + sendAuthEmailService.send( + SendAuthEmailArgs( + to = useCaseIn.email, + subject = "[FEW] 인증 이메일 주소를 확인해주세요.", + template = "auth", + content = Content( + headComment = headComment, + subComment = subComment, + email = email, + confirmLink = URL("https://www.fewletter.com/auth/validation/complete?auth_token=$token") + ) ) - ).let { - sendAuthEmailService.send(it) - } + ) }.onFailure { return SaveMemberUseCaseOut( isSendAuthEmail = false diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt index 991e310a..e0a05d34 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt @@ -62,12 +62,12 @@ class TokenUseCase( if (memberEmailAndTypeRecord.memberType == MemberType.PREAUTH) { isLogin = false - UpdateMemberTypeCommand( - id = memberId, - memberType = MemberType.NORMAL - ).let { command -> - memberDao.updateMemberType(command) - } + memberDao.updateMemberType( + UpdateMemberTypeCommand( + id = memberId, + memberType = MemberType.NORMAL + ) + ) } /** id가 요청에 포함되어 있으면 id를 통해 새로운 토큰을 발급 */ 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 60cde9b2..573093ab 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 @@ -13,10 +13,9 @@ class BrowseProblemsUseCase( ) { fun execute(useCaseIn: BrowseProblemsUseCaseIn): BrowseProblemsUseCaseOut { - SelectProblemsByArticleIdQuery(useCaseIn.articleId).let { query: SelectProblemsByArticleIdQuery -> - problemDao.selectProblemsByArticleId(query) ?: throw NotFoundException("problem.notfound.articleId") - }.let { - return BrowseProblemsUseCaseOut(it.problemIds) - } + problemDao.selectProblemsByArticleId(SelectProblemsByArticleIdQuery(useCaseIn.articleId)) + ?.let { + return BrowseProblemsUseCaseOut(it.problemIds) + } ?: throw NotFoundException("problem.notfound.articleId") } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt index f58ffddc..c3f55583 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCase.kt @@ -19,16 +19,16 @@ class CheckProblemUseCase( @Transactional fun execute(useCaseIn: CheckProblemUseCaseIn): CheckProblemUseCaseOut { + val memberId = useCaseIn.memberId val problemId = useCaseIn.problemId val submitAns = useCaseIn.sub val record = problemDao.selectProblemAnswer(SelectProblemAnswerQuery(problemId)) ?: throw NotFoundException("problem.notfound.id") val isSolved = record.answer == submitAns - val submitHistoryId = submitHistoryDao.insertSubmitHistory( - InsertSubmitHistoryCommand(problemId, 1L, submitAns, isSolved) + submitHistoryDao.insertSubmitHistory( + InsertSubmitHistoryCommand(problemId, memberId, submitAns, isSolved) ) ?: throw InsertException("submit.insertfail.record") - // not used 'submitHistoryId' return CheckProblemUseCaseOut( explanation = record.explanation, diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/CheckProblemUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/CheckProblemUseCaseIn.kt index 56637f72..66234a5a 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/CheckProblemUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/dto/CheckProblemUseCaseIn.kt @@ -1,6 +1,7 @@ package com.few.api.domain.problem.usecase.dto data class CheckProblemUseCaseIn( + val memberId: Long, val problemId: Long, val sub: String, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt index cd810d14..415c9462 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/handler/SendWorkbookArticleAsyncHandler.kt @@ -12,6 +12,7 @@ import com.few.api.repo.dao.subscription.command.UpdateArticleProgressCommand import com.few.api.repo.dao.subscription.command.UpdateLastArticleProgressCommand import com.few.data.common.code.CategoryType import com.few.email.service.article.dto.Content +import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component import java.time.LocalDate @@ -24,21 +25,23 @@ class SendWorkbookArticleAsyncHandler( private val subscriptionDao: SubscriptionDao, private val emailService: SubscriptionEmailService, ) { + private val log = KotlinLogging.logger {} @Async(value = DATABASE_ACCESS_POOL) fun sendWorkbookArticle(memberId: Long, workbookId: Long, articleDayCol: Byte) { val date = LocalDate.now() - val memberEmail = ReadMemberEmailInDto(memberId).let { memberService.readMemberEmail(it) }.let { it?.email } + val memberEmail = memberService.readMemberEmail(ReadMemberEmailInDto(memberId))?.email ?: throw NotFoundException("member.notfound.id") - val article = ReadArticleIdByWorkbookIdAndDayDto(workbookId, articleDayCol.toInt()).let { - articleService.readArticleIdByWorkbookIdAndDay(it) - }?.let { articleId -> - ReadArticleContentInDto(articleId).let { - articleService.readArticleContent(it) - } + val article = articleService.readArticleIdByWorkbookIdAndDay( + ReadArticleIdByWorkbookIdAndDayDto( + workbookId, + articleDayCol.toInt() + ) + )?.let { articleId -> + articleService.readArticleContent(ReadArticleContentInDto(articleId)) } ?: throw NotFoundException("article.notfound.id") - SendArticleInDto( + val sendArticleInDto = SendArticleInDto( memberId = memberId, workbookId = workbookId, toEmail = memberEmail, @@ -56,25 +59,35 @@ class SendWorkbookArticleAsyncHandler( writerLink = article.writerLink, articleContent = article.articleContent ) - ).let { - runCatching { emailService.sendArticleEmail(it) } - .onSuccess { - val lastDayArticleId = ReadWorkbookLastArticleIdInDto( - workbookId - ).let { - workbookService.readWorkbookLastArticleId(it) - }?.lastArticleId ?: throw NotFoundException("workbook.notfound.id") + ) + + runCatching { emailService.sendArticleEmail(sendArticleInDto) } + .onSuccess { + val lastDayArticleId = + workbookService.readWorkbookLastArticleId( + ReadWorkbookLastArticleIdInDto( + workbookId + ) + )?.lastArticleId ?: throw NotFoundException("workbook.notfound.id") - if (article.id == lastDayArticleId) { - UpdateArticleProgressCommand(workbookId, memberId).let { - subscriptionDao.updateArticleProgress(it) - } - } else { - UpdateLastArticleProgressCommand(workbookId, memberId).let { - subscriptionDao.updateLastArticleProgress(it) - } - } + if (article.id == lastDayArticleId) { + subscriptionDao.updateArticleProgress( + UpdateArticleProgressCommand( + workbookId, + memberId + ) + ) + } else { + subscriptionDao.updateLastArticleProgress( + UpdateLastArticleProgressCommand( + workbookId, + memberId + ) + ) } - } + } + .onFailure { + log.error(it) { "Failed to send article email" } + } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/handler/WorkbookSubscriptionClientAsyncHandler.kt b/api/src/main/kotlin/com/few/api/domain/subscription/handler/WorkbookSubscriptionClientAsyncHandler.kt index 0fb285ca..0019467c 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/handler/WorkbookSubscriptionClientAsyncHandler.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/handler/WorkbookSubscriptionClientAsyncHandler.kt @@ -19,18 +19,18 @@ class WorkbookSubscriptionClientAsyncHandler( @Async(value = DISCORD_HOOK_EVENT_POOL) fun sendSubscriptionEvent(workbookId: Long) { - val title = ReadWorkbookTitleInDto(workbookId).let { dto -> - workbookService.readWorkbookTitle(dto)?.workbookTitle + val title = + workbookService.readWorkbookTitle(ReadWorkbookTitleInDto(workbookId))?.workbookTitle ?: throw NotFoundException("workbook.notfound.id") - } - subscriptionDao.countAllSubscriptionStatus().let { record -> - WorkbookSubscriptionArgs( - totalSubscriptions = record.totalSubscriptions, - activeSubscriptions = record.activeSubscriptions, - workbookTitle = title - ).let { args -> - subscriptionClient.announceWorkbookSubscription(args) - } + + subscriptionDao.countAllSubscriptionStatus().also { record -> + subscriptionClient.announceWorkbookSubscription( + WorkbookSubscriptionArgs( + totalSubscriptions = record.totalSubscriptions, + activeSubscriptions = record.activeSubscriptions, + workbookTitle = title + ) + ) } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionArticleService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionArticleService.kt index a6068e27..21b70f97 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionArticleService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionArticleService.kt @@ -13,15 +13,16 @@ class SubscriptionArticleService( private val articleDao: ArticleDao, ) { fun readArticleIdByWorkbookIdAndDay(dto: ReadArticleIdByWorkbookIdAndDayDto): Long? { - SelectArticleIdByWorkbookIdAndDayQuery(dto.workbookId, dto.day).let { query -> - return articleDao.selectArticleIdByWorkbookIdAndDay(query) - } + return articleDao.selectArticleIdByWorkbookIdAndDay( + SelectArticleIdByWorkbookIdAndDayQuery( + dto.workbookId, + dto.day + ) + ) } fun readArticleContent(dto: ReadArticleContentInDto): ReadArticleContentOutDto? { - return SelectArticleContentQuery(dto.articleId).let { query -> - articleDao.selectArticleContent(query) - }?.let { + return articleDao.selectArticleContent(SelectArticleContentQuery(dto.articleId))?.let { ReadArticleContentOutDto( it.id, it.category, diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt index e1347fa4..2865ed18 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionEmailService.kt @@ -16,16 +16,16 @@ class SubscriptionEmailService( } fun sendArticleEmail(dto: SendArticleInDto) { - SendArticleEmailArgs( - dto.toEmail, - ARTICLE_SUBJECT_TEMPLATE.format( - dto.articleDayCol, - dto.articleTitle - ), - ARTICLE_TEMPLATE, - dto.articleContent - ).let { - sendArticleEmailService.send(it) - } + sendArticleEmailService.send( + SendArticleEmailArgs( + dto.toEmail, + ARTICLE_SUBJECT_TEMPLATE.format( + dto.articleDayCol, + dto.articleTitle + ), + ARTICLE_TEMPLATE, + dto.articleContent + ) + ) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionMemberService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionMemberService.kt index 43e49c4d..109480b8 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionMemberService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionMemberService.kt @@ -23,9 +23,7 @@ class SubscriptionMemberService( } fun readMemberEmail(dto: ReadMemberEmailInDto): ReadMemberEmailOutDto? { - return SelectMemberEmailQuery(dto.memberId).let { query -> - memberDao.selectMemberEmail(query) - }?.let { + return memberDao.selectMemberEmail(SelectMemberEmailQuery(dto.memberId))?.let { ReadMemberEmailOutDto(it) } } diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionWorkbookService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionWorkbookService.kt index b2890c41..967446fa 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionWorkbookService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/SubscriptionWorkbookService.kt @@ -1,10 +1,8 @@ package com.few.api.domain.subscription.service -import com.few.api.domain.subscription.service.dto.ReadWorkbookLastArticleIdInDto -import com.few.api.domain.subscription.service.dto.ReadWorkbookLastArticleIdOutDto -import com.few.api.domain.subscription.service.dto.ReadWorkbookTitleInDto -import com.few.api.domain.subscription.service.dto.ReadWorkbookTitleOutDto +import com.few.api.domain.subscription.service.dto.* import com.few.api.repo.dao.workbook.WorkbookDao +import com.few.api.repo.dao.workbook.query.SelectAllWorkbookTitleQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookLastArticleIdQuery import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery import org.springframework.stereotype.Service @@ -15,16 +13,28 @@ class SubscriptionWorkbookService( ) { fun readWorkbookTitle(dto: ReadWorkbookTitleInDto): ReadWorkbookTitleOutDto? { - return SelectWorkBookRecordQuery(dto.workbookId).let { query -> - workbookDao.selectWorkBook(query)?.title?.let { ReadWorkbookTitleOutDto(it) } - } + return workbookDao.selectWorkBook(SelectWorkBookRecordQuery(dto.workbookId)) + ?.title + ?.let { + ReadWorkbookTitleOutDto( + it + ) + } + } + + /** + * key: workbookId + * value: title + */ + fun readAllWorkbookTitle(dto: ReadAllWorkbookTitleInDto): Map { + return workbookDao.selectAllWorkbookTitle(SelectAllWorkbookTitleQuery(dto.workbookIds)) + .associateBy({ it.workbookId }, { it.title }) } fun readWorkbookLastArticleId(dto: ReadWorkbookLastArticleIdInDto): ReadWorkbookLastArticleIdOutDto? { - return SelectWorkBookLastArticleIdQuery(dto.workbookId).let { query -> - workbookDao.selectWorkBookLastArticleId(query) - }?.let { - ReadWorkbookLastArticleIdOutDto(it) - } + return workbookDao.selectWorkBookLastArticleId(SelectWorkBookLastArticleIdQuery(dto.workbookId)) + ?.let { + ReadWorkbookLastArticleIdOutDto(it) + } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/ReadAllWorkbookTitleInDto.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/ReadAllWorkbookTitleInDto.kt new file mode 100644 index 00000000..d662bb2c --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/dto/ReadAllWorkbookTitleInDto.kt @@ -0,0 +1,5 @@ +package com.few.api.domain.subscription.service.dto + +data class ReadAllWorkbookTitleInDto( + val workbookIds: List, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt index 6a039f5f..70b72404 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt @@ -2,81 +2,160 @@ package com.few.api.domain.subscription.usecase import com.fasterxml.jackson.databind.ObjectMapper import com.few.api.domain.subscription.service.SubscriptionArticleService +import com.few.api.domain.subscription.service.SubscriptionWorkbookService +import com.few.api.domain.subscription.service.dto.ReadAllWorkbookTitleInDto import com.few.api.domain.subscription.service.dto.ReadArticleIdByWorkbookIdAndDayDto -import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCaseIn -import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCaseOut -import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookDetail +import com.few.api.domain.subscription.usecase.dto.* import com.few.api.repo.dao.subscription.SubscriptionDao -import com.few.api.repo.dao.subscription.query.CountAllWorkbooksSubscription -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscription -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSubscription +import com.few.api.repo.dao.subscription.query.CountAllWorkbooksSubscriptionQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscriptionQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSubscriptionQuery +import com.few.api.repo.dao.subscription.query.SelectAllSubscriptionSendStatusQuery +import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookStatus import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.webjars.NotFoundException +import java.lang.IllegalStateException +enum class SUBSCRIBE_WORKBOOK_STRATEGY { + /** + * 로그인 상태에서 메인 화면에 보여질 워크북을 정렬합니다. + * - view의 값이 MAIN_CARD이다. + * */ + MAIN_CARD, + + /** + * 마이페이지에서 보여질 워크북을 정렬합니다. + * - view의 값이 MY_PAGE이다. + * */ + MY_PAGE, +} + +data class ArticleInfo( + // todo fix articleId to id + val articleId: Long, +) + +data class WorkbookInfo( + val id: Long, + val title: String, +) + +// todo refactor to model @Component class BrowseSubscribeWorkbooksUseCase( private val subscriptionDao: SubscriptionDao, private val subscriptionArticleService: SubscriptionArticleService, + private val subscriptionWorkbookService: SubscriptionWorkbookService, private val objectMapper: ObjectMapper, ) { @Transactional fun execute(useCaseIn: BrowseSubscribeWorkbooksUseCaseIn): BrowseSubscribeWorkbooksUseCaseOut { - val inActiveSubscriptionRecords = - SelectAllMemberWorkbookInActiveSubscription(useCaseIn.memberId).let { - subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(it) - } - - val activeSubscriptionRecords = - SelectAllMemberWorkbookActiveSubscription(useCaseIn.memberId).let { - subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(it) - } - - val subscriptionRecords = inActiveSubscriptionRecords + activeSubscriptionRecords - - /** - * key: workbookId - * value: workbook의 currentDay에 해당하는 articleId - */ - val workbookSubscriptionCurrentArticleIdRecords = subscriptionRecords.associate { it -> - val articleId = ReadArticleIdByWorkbookIdAndDayDto(it.workbookId, it.currentDay).let { - subscriptionArticleService.readArticleIdByWorkbookIdAndDay(it) - } ?: throw NotFoundException("article.notfound.workbookIdAndCurrentDay") - it.workbookId to articleId + val strategy = when (useCaseIn.view) { + ViewCategory.MAIN_CARD -> SUBSCRIBE_WORKBOOK_STRATEGY.MAIN_CARD + ViewCategory.MY_PAGE -> SUBSCRIBE_WORKBOOK_STRATEGY.MY_PAGE } - val subscriptionWorkbookIds = subscriptionRecords.map { it.workbookId } - val workbookSubscriptionCountRecords = - CountAllWorkbooksSubscription(subscriptionWorkbookIds).let { - subscriptionDao.countAllWorkbookSubscription(it) + val workbookStatusRecords = when (strategy) { + SUBSCRIBE_WORKBOOK_STRATEGY.MAIN_CARD -> { + val activeSubscriptionRecords = subscriptionDao.selectAllActiveWorkbookSubscriptionStatus( + SelectAllMemberWorkbookActiveSubscriptionQuery(useCaseIn.memberId) + ) + val inActiveSubscriptionRecords = + subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus( + SelectAllMemberWorkbookInActiveSubscriptionQuery(useCaseIn.memberId) + ) + activeSubscriptionRecords + inActiveSubscriptionRecords } - - subscriptionRecords.map { - /** - * 임시 코드 - * Batch 코드에서 currentDay가 totalDay보다 큰 경우가 발생하여 - * currentDay가 totalDay보다 크면 totalDay로 변경 - * */ - var currentDay = it.currentDay - if (it.currentDay > it.totalDay) { - currentDay = it.totalDay + SUBSCRIBE_WORKBOOK_STRATEGY.MY_PAGE -> { + subscriptionDao.selectAllActiveWorkbookSubscriptionStatus( + SelectAllMemberWorkbookActiveSubscriptionQuery(useCaseIn.memberId) + ) } + } + val subscriptionWorkbookIds = workbookStatusRecords.map { it.workbookId } + val subscriptionWorkbookCountRecords = subscriptionDao.countAllWorkbookSubscription( + CountAllWorkbooksSubscriptionQuery(subscriptionWorkbookIds) + ) + val subscriptionWorkbookSendStatusRecords = subscriptionDao.selectAllSubscriptionSendStatus( + SelectAllSubscriptionSendStatusQuery(useCaseIn.memberId, subscriptionWorkbookIds) + ).associateBy { it.workbookId } + + val workbookDetails = workbookStatusRecords.map { SubscribeWorkbookDetail( workbookId = it.workbookId, isActiveSub = WorkBookStatus.fromStatus(it.isActiveSub), - currentDay = currentDay, + currentDay = it.currentDay, totalDay = it.totalDay, - totalSubscriber = workbookSubscriptionCountRecords[it.workbookId]?.toLong() ?: 0, - articleInfo = objectMapper.writeValueAsString( - mapOf( - "articleId" to workbookSubscriptionCurrentArticleIdRecords[it.workbookId] + totalSubscriber = subscriptionWorkbookCountRecords[it.workbookId]?.toLong() ?: 0, + subscription = subscriptionWorkbookSendStatusRecords[it.workbookId]?.let { record -> + Subscription( + time = record.sendTime, + dateTimeCode = record.sendDay ) - ) + } ?: throw IllegalStateException("${it.workbookId}'s subscription send status is null") ) - }.let { - return BrowseSubscribeWorkbooksUseCaseOut(it) + } + + return when (strategy) { + SUBSCRIBE_WORKBOOK_STRATEGY.MAIN_CARD -> { + val workbookSubscriptionCurrentArticleIdRecords = workbookStatusRecords.associate { record -> + val articleId = subscriptionArticleService.readArticleIdByWorkbookIdAndDay( + ReadArticleIdByWorkbookIdAndDayDto(record.workbookId, record.currentDay) + ) ?: throw NotFoundException("article.notfound.workbookIdAndCurrentDay") + + record.workbookId to articleId + } + + BrowseSubscribeWorkbooksUseCaseOut( + clazz = MainCardSubscribeWorkbookDetail::class.java, + workbooks = workbookDetails.map { + MainCardSubscribeWorkbookDetail( + workbookId = it.workbookId, + isActiveSub = it.isActiveSub, + currentDay = it.currentDay, + totalDay = it.totalDay, + totalSubscriber = it.totalSubscriber, + subscription = it.subscription, + articleInfo = objectMapper.writeValueAsString( + ArticleInfo( + workbookSubscriptionCurrentArticleIdRecords[it.workbookId] + ?: throw IllegalStateException("${it.workbookId}'s articleId is null") + ) + ) + ) + } + ) + } + + SUBSCRIBE_WORKBOOK_STRATEGY.MY_PAGE -> { + val workbookTitleRecords = subscriptionWorkbookService.readAllWorkbookTitle( + ReadAllWorkbookTitleInDto(subscriptionWorkbookIds) + ) + + BrowseSubscribeWorkbooksUseCaseOut( + clazz = MyPageSubscribeWorkbookDetail::class.java, + workbooks = workbookDetails.map { + MyPageSubscribeWorkbookDetail( + workbookId = it.workbookId, + isActiveSub = it.isActiveSub, + currentDay = it.currentDay, + totalDay = it.totalDay, + totalSubscriber = it.totalSubscriber, + subscription = it.subscription, + workbookInfo = objectMapper.writeValueAsString( + WorkbookInfo( + id = it.workbookId, + title = workbookTitleRecords[it.workbookId] + ?: throw IllegalStateException("${it.workbookId}'s title is null") + ) + ) + ) + } + ) + } } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt index 209b12a2..7296d4f7 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt @@ -1,5 +1,7 @@ package com.few.api.domain.subscription.usecase +import com.few.api.domain.common.lock.LockFor +import com.few.api.domain.common.lock.LockIdentifier import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.InsertWorkbookSubscriptionCommand @@ -21,6 +23,7 @@ class SubscribeWorkbookUseCase( private val applicationEventPublisher: ApplicationEventPublisher, ) { + @LockFor(LockIdentifier.SUBSCRIPTION_MEMBER_ID_WORKBOOK_ID) @Transactional fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) { val subTargetWorkbookId = useCaseIn.workbookId @@ -31,23 +34,20 @@ class SubscribeWorkbookUseCase( ) val workbookSubscriptionHistory = subscriptionDao.selectTopWorkbookSubscriptionStatus( - SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery(memberId = memberId, workbookId = subTargetWorkbookId) - ).run { - if (this != null) { - WorkbookSubscriptionHistory( - false, - WorkbookSubscriptionStatus( - workbookId = this.workbookId, - isActiveSub = this.isActiveSub, - day = this.day - ) - ) - } else { - WorkbookSubscriptionHistory( - true + SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery( + memberId = memberId, + workbookId = subTargetWorkbookId + ) + )?.let { + WorkbookSubscriptionHistory( + false, + WorkbookSubscriptionStatus( + workbookId = it.workbookId, + isActiveSub = it.isActiveSub, + day = it.day ) - } - } + ) + } ?: WorkbookSubscriptionHistory(true) when { /** 구독한 히스토리가 없는 경우 */ @@ -58,9 +58,11 @@ class SubscribeWorkbookUseCase( /** 이미 구독한 히스토리가 있고 구독이 취소된 경우 */ workbookSubscriptionHistory.isCancelSub -> { val cancelledWorkbookSubscriptionHistory = CancelledWorkbookSubscriptionHistory(workbookSubscriptionHistory) - val lastDay = CountWorkbookMappedArticlesQuery(subTargetWorkbookId).let { - subscriptionDao.countWorkbookMappedArticles(it) - } ?: throw NotFoundException("workbook.notfound.id") + val lastDay = subscriptionDao.countWorkbookMappedArticles( + CountWorkbookMappedArticlesQuery( + subTargetWorkbookId + ) + ) ?: throw NotFoundException("workbook.notfound.id") if (cancelledWorkbookSubscriptionHistory.isSubEnd(lastDay)) { /** 이미 구독이 종료된 경우 */ diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt new file mode 100644 index 00000000..8531fb93 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionDayUseCase.kt @@ -0,0 +1,30 @@ +package com.few.api.domain.subscription.usecase + +import com.few.api.domain.subscription.usecase.dto.UpdateSubscriptionDayUseCaseIn +import com.few.api.repo.dao.subscription.SubscriptionDao +import com.few.api.repo.dao.subscription.command.BulkUpdateSubscriptionSendDayCommand +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class UpdateSubscriptionDayUseCase( + private val subscriptionDao: SubscriptionDao, +) { + @Transactional + fun execute(useCaseIn: UpdateSubscriptionDayUseCaseIn) { + /** + * workbookId기 없으면, memberId로 구독중인 모든 workbookId를 가져와서 해당하는 모든 workbookId의 구독요일을 변경한다. + */ + useCaseIn.workbookId ?: subscriptionDao.selectAllActiveSubscriptionWorkbookIds( + SubscriptionDao.SelectAllActiveSubscriptionWorkbookIdsQuery(useCaseIn.memberId) + ).let { + subscriptionDao.bulkUpdateSubscriptionSendDay( + BulkUpdateSubscriptionSendDayCommand( + useCaseIn.memberId, + useCaseIn.dayCode.code, + it + ) + ) + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt new file mode 100644 index 00000000..70b69649 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UpdateSubscriptionTimeUseCase.kt @@ -0,0 +1,31 @@ +package com.few.api.domain.subscription.usecase + +import com.few.api.domain.subscription.usecase.dto.UpdateSubscriptionTimeUseCaseIn +import com.few.api.repo.dao.subscription.SubscriptionDao +import com.few.api.repo.dao.subscription.command.BulkUpdateSubscriptionSendTimeCommand +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +class UpdateSubscriptionTimeUseCase( + private val subscriptionDao: SubscriptionDao, + +) { + @Transactional + fun execute(useCaseIn: UpdateSubscriptionTimeUseCaseIn) { + /** + * workbookId기 없으면, memberId로 구독중인 모든 workbookId를 가져와서 해당하는 모든 workbookId의 구독요일을 변경한다. + */ + useCaseIn.workbookId ?: subscriptionDao.selectAllActiveSubscriptionWorkbookIds( + SubscriptionDao.SelectAllActiveSubscriptionWorkbookIdsQuery(useCaseIn.memberId) + ).let { + subscriptionDao.bulkUpdateSubscriptionSendTime( + BulkUpdateSubscriptionSendTimeCommand( + useCaseIn.memberId, + useCaseIn.time, + it + ) + ) + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseIn.kt index 7fb389b9..e59b4728 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseIn.kt @@ -1,5 +1,8 @@ package com.few.api.domain.subscription.usecase.dto +import com.few.api.web.support.ViewCategory + data class BrowseSubscribeWorkbooksUseCaseIn( val memberId: Long, + val view: ViewCategory, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseOut.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseOut.kt index 5ec18250..5138f197 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseOut.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/BrowseSubscribeWorkbooksUseCaseOut.kt @@ -1,17 +1,65 @@ package com.few.api.domain.subscription.usecase.dto +import com.fasterxml.jackson.annotation.JsonFormat +import com.few.api.web.support.DayCode import com.few.api.web.support.WorkBookStatus +import java.time.LocalTime data class BrowseSubscribeWorkbooksUseCaseOut( val workbooks: List, + val clazz: Class, ) -data class SubscribeWorkbookDetail( +open class SubscribeWorkbookDetail( val workbookId: Long, val isActiveSub: WorkBookStatus, val currentDay: Int, val totalDay: Int, val rank: Long = 0, val totalSubscriber: Long, + val subscription: Subscription, +) + +data class Subscription( + @JsonFormat(pattern = "HH:mm") + val time: LocalTime = LocalTime.of(0, 0), + val dateTimeCode: String = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code, +) + +class MainCardSubscribeWorkbookDetail( + workbookId: Long, + isActiveSub: WorkBookStatus, + currentDay: Int, + totalDay: Int, + rank: Long = 0, + totalSubscriber: Long, + subscription: Subscription, val articleInfo: String = "{}", +) : SubscribeWorkbookDetail( + workbookId = workbookId, + isActiveSub = isActiveSub, + currentDay = currentDay, + totalDay = totalDay, + rank = rank, + totalSubscriber = totalSubscriber, + subscription = subscription +) + +class MyPageSubscribeWorkbookDetail( + workbookId: Long, + isActiveSub: WorkBookStatus, + currentDay: Int, + totalDay: Int, + rank: Long = 0, + totalSubscriber: Long, + subscription: Subscription, + val workbookInfo: String = "{}", +) : SubscribeWorkbookDetail( + workbookId = workbookId, + isActiveSub = isActiveSub, + currentDay = currentDay, + totalDay = totalDay, + rank = rank, + totalSubscriber = totalSubscriber, + subscription = subscription ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionDayUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionDayUseCaseIn.kt new file mode 100644 index 00000000..6d92824f --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionDayUseCaseIn.kt @@ -0,0 +1,9 @@ +package com.few.api.domain.subscription.usecase.dto + +import com.few.api.web.support.DayCode + +data class UpdateSubscriptionDayUseCaseIn( + val memberId: Long, + val dayCode: DayCode, + val workbookId: Long?, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionTimeUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionTimeUseCaseIn.kt new file mode 100644 index 00000000..649b0054 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UpdateSubscriptionTimeUseCaseIn.kt @@ -0,0 +1,9 @@ +package com.few.api.domain.subscription.usecase.dto + +import java.time.LocalTime + +data class UpdateSubscriptionTimeUseCaseIn( + val memberId: Long, + val time: LocalTime, + val workbookId: Long?, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt index dfa7f209..e975c672 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt @@ -27,22 +27,19 @@ class ReadWorkBookArticleUseCase( ) { @Transactional(readOnly = true) fun execute(useCaseIn: ReadWorkBookArticleUseCaseIn): ReadWorkBookArticleOut { - val articleRecord = SelectWorkBookArticleRecordQuery( - useCaseIn.workbookId, - useCaseIn.articleId - ).let { query: SelectWorkBookArticleRecordQuery -> - articleDao.selectWorkBookArticleRecord(query) - } ?: throw NotFoundException("article.notfound.articleidworkbookid") + val articleRecord = articleDao.selectWorkBookArticleRecord( + SelectWorkBookArticleRecordQuery( + useCaseIn.workbookId, + useCaseIn.articleId + ) + ) ?: throw NotFoundException("article.notfound.articleidworkbookid") val writerRecord = - ReadWriterRecordInDto(articleRecord.writerId).let { query: ReadWriterRecordInDto -> - readArticleWriterRecordService.execute(query) ?: throw NotFoundException("writer.notfound.id") - } + readArticleWriterRecordService.execute(ReadWriterRecordInDto(articleRecord.writerId)) + ?: throw NotFoundException("writer.notfound.id") val problemIds = - BrowseArticleProblemIdsInDto(articleRecord.articleId).let { query: BrowseArticleProblemIdsInDto -> - browseArticleProblemsService.execute(query) - } + browseArticleProblemsService.execute(BrowseArticleProblemIdsInDto(articleRecord.articleId)) /** * @see com.few.api.domain.article.usecase.ReadArticleUseCase diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookArticleService.kt b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookArticleService.kt index b7a4d830..e0985abc 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookArticleService.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookArticleService.kt @@ -20,18 +20,20 @@ class WorkbookArticleService( private val articleDao: ArticleDao, ) { fun browseWorkbookArticles(query: BrowseWorkbookArticlesInDto): List { - return SelectWorkbookMappedArticleRecordsQuery(query.workbookId).let { query -> - articleDao.selectWorkbookMappedArticleRecords(query).map { record -> - WorkBookArticleOutDto( - articleId = record.articleId, - writerId = record.writerId, - mainImageURL = record.mainImageURL, - title = record.title, - category = record.category, - content = record.content, - createdAt = record.createdAt - ) - } + return articleDao.selectWorkbookMappedArticleRecords( + SelectWorkbookMappedArticleRecordsQuery( + query.workbookId + ) + ).map { record -> + WorkBookArticleOutDto( + articleId = record.articleId, + writerId = record.writerId, + mainImageURL = record.mainImageURL, + title = record.title, + category = record.category, + content = record.content, + createdAt = record.createdAt + ) } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookMemberService.kt b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookMemberService.kt index 5786a8fc..feaaf900 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookMemberService.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookMemberService.kt @@ -19,14 +19,12 @@ class WorkbookMemberService( private val memberDao: MemberDao, ) { fun browseWriterRecords(query: BrowseWriterRecordsInDto): List { - return SelectWritersQuery(query.writerIds).let { query -> - memberDao.selectWriters(query).map { record -> - WriterOutDto( - writerId = record.writerId, - name = record.name, - url = record.url - ) - } + return memberDao.selectWriters(SelectWritersQuery(query.writerIds)).map { record -> + WriterOutDto( + writerId = record.writerId, + name = record.name, + url = record.url + ) } } @@ -35,8 +33,8 @@ class WorkbookMemberService( * value: writer list */ fun browseWorkbookWriterRecords(query: BrowseWorkbookWriterRecordsInDto): Map> { - return BrowseWorkbookWritersQuery(query.workbookIds).let { query -> - memberDao.selectWriters(query).map { record -> + return memberDao.selectWriters(BrowseWorkbookWritersQuery(query.workbookIds)) + .map { record -> WriterMappedWorkbookOutDto( workbookId = record.workbookId, writerId = record.writerId, @@ -44,6 +42,5 @@ class WorkbookMemberService( url = record.url ) }.groupBy { it.workbookId } - } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt index 71fb291b..3bbd89ae 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt @@ -3,8 +3,8 @@ package com.few.api.domain.workbook.service import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksInDto import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksOutDto import com.few.api.repo.dao.subscription.SubscriptionDao -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscription -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSubscription +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscriptionQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSubscriptionQuery import org.springframework.stereotype.Service @Service @@ -14,14 +14,13 @@ class WorkbookSubscribeService( fun browseMemberSubscribeWorkbooks(dto: BrowseMemberSubscribeWorkbooksInDto): List { val inActiveSubscriptionRecords = - SelectAllMemberWorkbookInActiveSubscription(dto.memberId).let { - subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(it) - } + subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus( + SelectAllMemberWorkbookInActiveSubscriptionQuery(dto.memberId) + ) - val activeSubscriptionRecords = - SelectAllMemberWorkbookActiveSubscription(dto.memberId).let { - subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(it) - } + val activeSubscriptionRecords = subscriptionDao.selectAllActiveWorkbookSubscriptionStatus( + SelectAllMemberWorkbookActiveSubscriptionQuery(dto.memberId) + ) val subscriptionRecords = inActiveSubscriptionRecords + activeSubscriptionRecords diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt index 61f10409..5aa097dc 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt @@ -13,7 +13,7 @@ import com.few.api.domain.workbook.usecase.service.order.AuthMainViewWorkbookOrd import com.few.api.domain.workbook.usecase.service.order.BasicWorkbookOrderDelegator import com.few.api.domain.workbook.usecase.service.order.WorkbookOrderDelegatorExecutor import com.few.api.repo.dao.workbook.WorkbookDao -import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCount +import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCountQuery import com.few.api.web.support.ViewCategory import com.few.data.common.code.CategoryType import org.springframework.stereotype.Component @@ -47,14 +47,14 @@ class BrowseWorkbooksUseCase( @Transactional fun execute(useCaseIn: BrowseWorkbooksUseCaseIn): BrowseWorkbooksUseCaseOut { - val workbookRecords = BrowseWorkBookQueryWithSubscriptionCount(useCaseIn.category.code).let { query -> - workbookDao.browseWorkBookWithSubscriptionCount(query) - } + val workbookRecords = workbookDao.browseWorkBookWithSubscriptionCount( + BrowseWorkBookQueryWithSubscriptionCountQuery(useCaseIn.category.code) + ) val workbookIds = workbookRecords.map { it.id } - val writerRecords = BrowseWorkbookWriterRecordsInDto(workbookIds).let { query -> - workbookMemberService.browseWorkbookWriterRecords(query) - } + val writerRecords = workbookMemberService.browseWorkbookWriterRecords( + BrowseWorkbookWriterRecordsInDto(workbookIds) + ) val workbookDetails = workbookRecords.map { record -> WorkBook( diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCase.kt index 1d70fd0e..b2e4b160 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCase.kt @@ -21,17 +21,17 @@ class ReadWorkbookUseCase( fun execute(useCaseIn: ReadWorkbookUseCaseIn): ReadWorkbookUseCaseOut { val workbookId = useCaseIn.workbookId - val workbookRecord = SelectWorkBookRecordQuery(workbookId).let { query -> - workbookDao.selectWorkBook(query) ?: throw NotFoundException("workbook.notfound.id") - } + val workbookRecord = workbookDao.selectWorkBook(SelectWorkBookRecordQuery(workbookId)) + ?: throw NotFoundException("workbook.notfound.id") - val workbookMappedArticles = BrowseWorkbookArticlesInDto(workbookId).let { query -> - workbookArticleService.browseWorkbookArticles(query) - } + val workbookMappedArticles = + workbookArticleService.browseWorkbookArticles(BrowseWorkbookArticlesInDto(workbookId)) - val writerRecords = BrowseWriterRecordsInDto(workbookMappedArticles.writerIds()).let { query -> - workbookMemberService.browseWriterRecords(query) - } + val writerRecords = workbookMemberService.browseWriterRecords( + BrowseWriterRecordsInDto( + workbookMappedArticles.writerIds() + ) + ) return ReadWorkbookUseCaseOut( id = workbookRecord.id, diff --git a/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt b/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt new file mode 100644 index 00000000..e2ca7f59 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt @@ -0,0 +1,20 @@ +package com.few.api.security.authentication.authority + +import org.apache.commons.lang3.StringUtils +import org.springframework.security.core.GrantedAuthority + +object AuthorityUtils { + + @Throws(IllegalArgumentException::class) + fun toAuthorities(roles: String): List { + val tokens = StringUtils.splitPreserveAllTokens(roles, "[,]") + val rtn: MutableList = ArrayList() + for (token in tokens) { + if (token != "") { + val role = token.trim { it <= ' ' } + rtn.add(Roles.valueOf(role).authority) + } + } + return rtn + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt index 4fbede4c..f3eb5514 100644 --- a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt +++ b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt @@ -3,7 +3,7 @@ package com.few.api.security.authentication.token import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails -class TokenUserDetails( +open class TokenUserDetails( val authorities: List, val id: String, val email: String, diff --git a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt index 69c9664e..66352a94 100644 --- a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt +++ b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt @@ -1,12 +1,10 @@ package com.few.api.security.authentication.token -import com.few.api.security.authentication.authority.Roles +import com.few.api.security.authentication.authority.AuthorityUtils import com.few.api.security.exception.AccessTokenInvalidException import com.few.api.security.token.TokenResolver import io.github.oshai.kotlinlogging.KotlinLogging import io.jsonwebtoken.Claims -import org.apache.commons.lang3.StringUtils -import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Component @@ -44,24 +42,8 @@ class TokenUserDetailsService( String::class.java ) - val authorities = toAuthorities(roles) + val authorities = AuthorityUtils.toAuthorities(roles) return TokenUserDetails(authorities, id.toString(), email) } - - private fun toAuthorities(roles: String): List { - val tokens = StringUtils.splitPreserveAllTokens(roles, "[,]") - val rtn: MutableList = ArrayList() - for (token in tokens) { - if (token != "") { - val role = token.trim { it <= ' ' } - try { - rtn.add(Roles.valueOf(role).authority) - } catch (exception: IllegalArgumentException) { - log.error { "${"Invalid role. role: {}"} $role" } - } - } - } - return rtn - } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt index 0b0dfccc..58f303b9 100644 --- a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt +++ b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt @@ -6,6 +6,7 @@ import com.few.api.security.filter.token.TokenAuthenticationFilter import com.few.api.security.handler.DelegatedAccessDeniedHandler import com.few.api.security.handler.DelegatedAuthenticationEntryPoint import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile import org.springframework.http.HttpMethod import org.springframework.security.authentication.ProviderManager @@ -17,14 +18,13 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter import org.springframework.security.web.util.matcher.AntPathRequestMatcher -import org.springframework.stereotype.Component import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource import org.springframework.web.filter.OncePerRequestFilter @EnableWebSecurity -@Component +@Configuration class WebSecurityConfig( private val authenticationEntryPoint: DelegatedAuthenticationEntryPoint, private val accessDeniedHandler: DelegatedAccessDeniedHandler, diff --git a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt index 494650b4..a9b17fca 100644 --- a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt +++ b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt @@ -1,16 +1,21 @@ package com.few.api.web.config +import com.few.api.web.config.converter.DayCodeConverter import com.few.api.web.config.converter.ViewConverter import com.few.api.web.config.converter.WorkBookCategoryConverter +import com.few.api.web.support.method.UserArgumentHandlerMethodArgumentResolver import org.springframework.context.annotation.Configuration import org.springframework.format.FormatterRegistry import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration -class WebConfig : WebMvcConfigurer { +class WebConfig( + private val userArgumentHandlerMethodArgumentResolver: UserArgumentHandlerMethodArgumentResolver, +) : WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") .allowedOriginPatterns(CorsConfiguration.ALL) @@ -28,5 +33,10 @@ class WebConfig : WebMvcConfigurer { override fun addFormatters(registry: FormatterRegistry) { registry.addConverter(WorkBookCategoryConverter()) registry.addConverter(ViewConverter()) + registry.addConverter(DayCodeConverter()) + } + + override fun addArgumentResolvers(argumentResolvers: MutableList) { + argumentResolvers.add(userArgumentHandlerMethodArgumentResolver) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/config/converter/DayCodeConverter.kt b/api/src/main/kotlin/com/few/api/web/config/converter/DayCodeConverter.kt new file mode 100644 index 00000000..35645d02 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/config/converter/DayCodeConverter.kt @@ -0,0 +1,10 @@ +package com.few.api.web.config.converter + +import com.few.api.web.support.DayCode +import org.springframework.core.convert.converter.Converter + +class DayCodeConverter : Converter { + override fun convert(source: String): DayCode { + return DayCode.fromCode(source) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt index c5ce0f12..ecb4d6f0 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt @@ -4,16 +4,15 @@ import com.few.api.domain.article.usecase.ReadArticleUseCase import com.few.api.domain.article.usecase.BrowseArticlesUseCase import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.article.response.ReadArticleResponse import com.few.api.web.controller.article.response.ReadArticlesResponse import com.few.api.web.controller.article.response.WorkbookInfo import com.few.api.web.controller.article.response.WriterInfo import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import com.few.data.common.code.CategoryType -import jakarta.servlet.http.HttpServletRequest import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -26,22 +25,16 @@ import org.springframework.web.bind.annotation.* class ArticleController( private val readArticleUseCase: ReadArticleUseCase, private val browseArticlesUseCase: BrowseArticlesUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readArticle( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @PathVariable(value = "articleId") @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } ?: 0L + val memberId = userArgumentDetails.id.toLong() val useCaseOut = ReadArticleUseCaseIn( articleId = articleId, diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/response/ReadArticleResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/article/response/ReadArticleResponse.kt index fa609cfa..ecb76a62 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/article/response/ReadArticleResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/article/response/ReadArticleResponse.kt @@ -1,6 +1,5 @@ package com.few.api.web.controller.article.response -import com.fasterxml.jackson.annotation.JsonFormat import java.net.URL import java.time.LocalDateTime @@ -12,7 +11,6 @@ data class ReadArticleResponse( val content: String, val problemIds: List, val category: String, - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val views: Long, val workbooks: List = emptyList(), 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 99124c5d..cc9e366e 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 @@ -13,6 +13,8 @@ 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.web.controller.problem.response.BrowseProblemsResponse +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import jakarta.validation.Valid import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus @@ -66,13 +68,21 @@ class ProblemController( @PostMapping("/{problemId}") fun checkProblem( + @UserArgument userArgumentDetails: UserArgumentDetails, @PathVariable(value = "problemId") @Min(value = 1, message = "{min.id}") problemId: Long, @Valid @RequestBody body: CheckProblemRequest, ): ApiResponse> { - val useCaseOut = checkProblemUseCase.execute(CheckProblemUseCaseIn(problemId, body.sub)) + val memberId = userArgumentDetails.id.toLong() + val useCaseOut = checkProblemUseCase.execute( + CheckProblemUseCaseIn( + memberId, + problemId, + body.sub + ) + ) val response = CheckProblemResponse( explanation = useCaseOut.explanation, diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt index 88bf98f5..f7408e8b 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt @@ -1,29 +1,17 @@ package com.few.api.web.controller.subscription -import com.few.api.domain.subscription.usecase.BrowseSubscribeWorkbooksUseCase +import com.few.api.domain.subscription.usecase.* import com.few.api.web.controller.subscription.request.UnsubscribeWorkbookRequest import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator -import com.few.api.domain.subscription.usecase.SubscribeWorkbookUseCase -import com.few.api.domain.subscription.usecase.UnsubscribeAllUseCase -import com.few.api.domain.subscription.usecase.UnsubscribeWorkbookUseCase -import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCaseIn -import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookUseCaseIn -import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn -import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn -import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase -import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn +import com.few.api.domain.subscription.usecase.dto.* import com.few.api.security.authentication.token.TokenUserDetails -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.subscription.request.UnsubscribeAllRequest -import com.few.api.web.controller.subscription.response.MainViewBrowseSubscribeWorkbooksResponse -import com.few.api.web.controller.subscription.response.MainViewSubscribeWorkbookInfo -import com.few.api.web.controller.subscription.response.SubscribeWorkbookInfo -import com.few.api.web.controller.subscription.response.SubscribeWorkbooksResponse +import com.few.api.web.controller.subscription.request.UpdateSubscriptionDayRequest +import com.few.api.web.controller.subscription.request.UpdateSubscriptionTimeRequest +import com.few.api.web.controller.subscription.response.* +import com.few.api.web.support.DayCode import com.few.api.web.support.ViewCategory -import com.few.api.web.support.WorkBookCategory -import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus @@ -31,6 +19,7 @@ import org.springframework.http.MediaType import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* +import java.lang.IllegalStateException @Validated @RestController @@ -40,10 +29,8 @@ class SubscriptionController( private val unsubscribeWorkbookUseCase: UnsubscribeWorkbookUseCase, private val unsubscribeAllUseCase: UnsubscribeAllUseCase, private val browseSubscribeWorkbooksUseCase: BrowseSubscribeWorkbooksUseCase, - private val tokenResolver: TokenResolver, - - // 임시 구현용 - private val browseWorkBooksUseCase: BrowseWorkbooksUseCase, + private val updateSubscriptionDayUseCase: UpdateSubscriptionDayUseCase, + private val updateSubscriptionTimeUseCase: UpdateSubscriptionTimeUseCase, ) { @GetMapping("/subscriptions/workbooks") @@ -52,110 +39,45 @@ class SubscriptionController( @RequestParam( value = "view", required = false - ) view: ViewCategory? = ViewCategory.MAIN_CARD, + ) view: ViewCategory?, ): ApiResponse> { val memberId = userDetails.username.toLong() - val useCaseOut = BrowseSubscribeWorkbooksUseCaseIn(memberId).let { - browseSubscribeWorkbooksUseCase.execute(it) - } - - return SubscribeWorkbooksResponse( - workbooks = useCaseOut.workbooks.map { - SubscribeWorkbookInfo( - id = it.workbookId, - currentDay = it.currentDay, - totalDay = it.totalDay, - status = it.isActiveSub.name, - rank = it.rank, - totalSubscriber = it.totalSubscriber, - articleInfo = it.articleInfo - ) - } - ).let { - ApiResponseGenerator.success(it, HttpStatus.OK) - } - } - - @GetMapping("/subscriptions/workbooks/main") - fun mainViewBrowseSubscribeWorkbooks( - servletRequest: HttpServletRequest, - @RequestParam(value = "category", required = false) - category: WorkBookCategory?, - ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } - - if (memberId != null) { - val memberSubscribeWorkbooks = BrowseSubscribeWorkbooksUseCaseIn(memberId).let { + val useCaseOut = + BrowseSubscribeWorkbooksUseCaseIn(memberId, view ?: ViewCategory.MAIN_CARD).let { browseSubscribeWorkbooksUseCase.execute(it) } - val workbooks = - BrowseWorkbooksUseCaseIn( - category ?: WorkBookCategory.All, - ViewCategory.MAIN_CARD, - memberId - ).let { useCaseIn -> - browseWorkBooksUseCase.execute(useCaseIn) - } - return MainViewBrowseSubscribeWorkbooksResponse( - workbooks = workbooks.workbooks.map { - MainViewSubscribeWorkbookInfo( - id = it.id, - mainImageUrl = it.mainImageUrl, - title = it.title, - description = it.description, - category = it.category, - createdAt = it.createdAt, - writerDetails = it.writerDetails, - subscriptionCount = it.subscriptionCount, - status = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.isActiveSub?.name, - totalDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalDay, - currentDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.currentDay, - rank = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.rank, - totalSubscriber = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalSubscriber, - articleInfo = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.articleInfo + // todo fix to facade usecase + return SubscribeWorkbooksResponse( + workbooks = when (useCaseOut.clazz) { + MainCardSubscribeWorkbookDetail::class.java -> useCaseOut.workbooks.map { it as MainCardSubscribeWorkbookDetail }.map { + MainCardSubscribeWorkbookInfo( + id = it.workbookId, + status = it.isActiveSub.name, + totalDay = it.totalDay, + currentDay = it.currentDay, + rank = it.rank, + totalSubscriber = it.totalSubscriber, + subscription = it.subscription, + articleInfo = it.articleInfo ) } - ).let { - ApiResponseGenerator.success(it, HttpStatus.OK) - } - } else { - val workbooks = - BrowseWorkbooksUseCaseIn( - category ?: WorkBookCategory.All, - ViewCategory.MAIN_CARD, - memberId - ).let { useCaseIn -> - browseWorkBooksUseCase.execute(useCaseIn) - } - - return MainViewBrowseSubscribeWorkbooksResponse( - workbooks = workbooks.workbooks.map { - MainViewSubscribeWorkbookInfo( - id = it.id, - mainImageUrl = it.mainImageUrl, - title = it.title, - description = it.description, - category = it.category, - createdAt = it.createdAt, - writerDetails = it.writerDetails, - subscriptionCount = it.subscriptionCount, - status = null, - totalDay = null, - currentDay = null, - rank = null, - totalSubscriber = null, - articleInfo = null + MyPageSubscribeWorkbookDetail::class.java -> useCaseOut.workbooks.map { it as MyPageSubscribeWorkbookDetail }.map { + MyPageSubscribeWorkbookInfo( + id = it.workbookId, + status = it.isActiveSub.name, + totalDay = it.totalDay, + currentDay = it.currentDay, + rank = it.rank, + totalSubscriber = it.totalSubscriber, + subscription = it.subscription, + workbookInfo = it.workbookInfo ) } - ).let { - ApiResponseGenerator.success(it, HttpStatus.OK) + else -> throw IllegalStateException("Invalid class type") } + ).let { + ApiResponseGenerator.success(it, HttpStatus.OK) } } @@ -183,14 +105,14 @@ class SubscriptionController( @Min(value = 1, message = "{min.id}") workbookId: Long, @Valid @RequestBody - body: UnsubscribeWorkbookRequest, + body: UnsubscribeWorkbookRequest?, ): ApiResponse { val memberId = userDetails.username.toLong() unsubscribeWorkbookUseCase.execute( UnsubscribeWorkbookUseCaseIn( workbookId = workbookId, memberId = memberId, - opinion = body.opinion + opinion = body?.opinion ?: "cancel" ) ) @@ -201,13 +123,51 @@ class SubscriptionController( fun deactivateAllSubscriptions( @AuthenticationPrincipal userDetails: TokenUserDetails, @Valid @RequestBody - body: UnsubscribeAllRequest, + body: UnsubscribeAllRequest?, ): ApiResponse { val memberId = userDetails.username.toLong() unsubscribeAllUseCase.execute( - UnsubscribeAllUseCaseIn(memberId = memberId, opinion = body.opinion) + UnsubscribeAllUseCaseIn(memberId = memberId, opinion = body?.opinion ?: "cancel") ) return ApiResponseGenerator.success(HttpStatus.OK) } + + @PatchMapping("/subscriptions/time") + fun updateSubscriptionTime( + @AuthenticationPrincipal userDetails: TokenUserDetails, + @Valid @RequestBody + body: UpdateSubscriptionTimeRequest, + ): ApiResponse { + UpdateSubscriptionTimeUseCaseIn( + memberId = userDetails.username.toLong(), + time = body.time, + workbookId = body.workbookId + ).let { + updateSubscriptionTimeUseCase.execute(it) + } + return ApiResponseGenerator.success(HttpStatus.OK) + } + + @PatchMapping("/subscriptions/day") + fun updateSubscriptionDay( + @AuthenticationPrincipal userDetails: TokenUserDetails, + @Valid @RequestBody + body: UpdateSubscriptionDayRequest, + ): ApiResponse { + val dayCode = DayCode.fromCode(body.dayCode) + dayCode.also { + if (!(it == (DayCode.MON_TUE_WED_THU_FRI_SAT_SUN) || it == (DayCode.MON_TUE_WED_THU_FRI))) { + throw IllegalArgumentException("Invalid day code") + } + } + UpdateSubscriptionDayUseCaseIn( + memberId = userDetails.username.toLong(), + dayCode = dayCode, + workbookId = body.workbookId + ).let { + updateSubscriptionDayUseCase.execute(it) + } + return ApiResponseGenerator.success(HttpStatus.OK) + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionDayRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionDayRequest.kt new file mode 100644 index 00000000..e07ba193 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionDayRequest.kt @@ -0,0 +1,9 @@ +package com.few.api.web.controller.subscription.request + +import com.fasterxml.jackson.annotation.JsonProperty + +data class UpdateSubscriptionDayRequest( + @JsonProperty("date") + val dayCode: String, + val workbookId: Long?, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionTimeRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionTimeRequest.kt new file mode 100644 index 00000000..eb73d89f --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UpdateSubscriptionTimeRequest.kt @@ -0,0 +1,8 @@ +package com.few.api.web.controller.subscription.request + +import java.time.LocalTime + +data class UpdateSubscriptionTimeRequest( + val time: LocalTime, + val workbookId: Long?, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/response/MainViewBrowseSubscribeWorkbooksResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/response/MainViewBrowseSubscribeWorkbooksResponse.kt deleted file mode 100644 index ccf23008..00000000 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/response/MainViewBrowseSubscribeWorkbooksResponse.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.few.api.web.controller.subscription.response - -import com.fasterxml.jackson.annotation.JsonFormat -import com.fasterxml.jackson.annotation.JsonInclude -import com.few.api.domain.workbook.usecase.dto.WriterDetail -import java.net.URL -import java.time.LocalDateTime - -data class MainViewBrowseSubscribeWorkbooksResponse( - val workbooks: List, -) - -@JsonInclude(JsonInclude.Include.NON_NULL) -data class MainViewSubscribeWorkbookInfo( - val id: Long, - val mainImageUrl: URL, - val title: String, - val description: String, - val category: String, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") - val createdAt: LocalDateTime, - val writerDetails: List, - val subscriptionCount: Long, - val status: String?, // convert from enum - val totalDay: Int?, - val currentDay: Int?, - val rank: Long?, - val totalSubscriber: Long?, - val articleInfo: String?, // convert from Json -) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/response/SubscribeWorkbooksResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/response/SubscribeWorkbooksResponse.kt index f7ad75da..e8c51f61 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/response/SubscribeWorkbooksResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/response/SubscribeWorkbooksResponse.kt @@ -1,15 +1,55 @@ package com.few.api.web.controller.subscription.response +import com.few.api.domain.subscription.usecase.dto.Subscription + data class SubscribeWorkbooksResponse( val workbooks: List, ) -data class SubscribeWorkbookInfo( +open class SubscribeWorkbookInfo( val id: Long, val status: String, // convert from enum val totalDay: Int, val currentDay: Int, val rank: Long, val totalSubscriber: Long, - val articleInfo: String, // convert from Json + val subscription: Subscription, +) + +class MainCardSubscribeWorkbookInfo( + id: Long, + status: String, + totalDay: Int, + currentDay: Int, + rank: Long, + totalSubscriber: Long, + subscription: Subscription, + val articleInfo: String, +) : SubscribeWorkbookInfo( + id = id, + status = status, + totalDay = totalDay, + currentDay = currentDay, + rank = rank, + totalSubscriber = totalSubscriber, + subscription = subscription +) + +class MyPageSubscribeWorkbookInfo( + id: Long, + status: String, + totalDay: Int, + currentDay: Int, + rank: Long, + totalSubscriber: Long, + subscription: Subscription, + val workbookInfo: String, +) : SubscribeWorkbookInfo( + id = id, + status = status, + totalDay = totalDay, + currentDay = currentDay, + rank = rank, + totalSubscriber = totalSubscriber, + subscription = subscription ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt index c978841b..1e1b45fa 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt @@ -4,14 +4,13 @@ import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseIn import com.few.api.domain.workbook.usecase.ReadWorkbookUseCase import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.response.* import com.few.api.web.support.WorkBookCategory import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator import com.few.api.web.support.ViewCategory -import jakarta.servlet.http.HttpServletRequest +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -28,7 +27,6 @@ import org.springframework.web.bind.annotation.RestController class WorkBookController( private val readWorkbookUseCase: ReadWorkbookUseCase, private val browseWorkBooksUseCase: BrowseWorkbooksUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/categories") @@ -48,18 +46,14 @@ class WorkBookController( @GetMapping fun browseWorkBooks( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @RequestParam(value = "category", required = false) category: WorkBookCategory?, @RequestParam(value = "view", required = false) viewCategory: ViewCategory?, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } + val memberId = userArgumentDetails.id.toLong() + val useCaseOut = BrowseWorkbooksUseCaseIn(category ?: WorkBookCategory.All, viewCategory, memberId).let { useCaseIn -> browseWorkBooksUseCase.execute(useCaseIn) diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt index e1cf175e..049d8de0 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt @@ -2,12 +2,11 @@ package com.few.api.web.controller.workbook.article import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleUseCaseIn import com.few.api.domain.workbook.article.usecase.ReadWorkBookArticleUseCase -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.article.response.ReadWorkBookArticleResponse import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator -import jakarta.servlet.http.HttpServletRequest +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -22,12 +21,11 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping(value = ["/api/v1/workbooks/{workbookId}/articles"], produces = [MediaType.APPLICATION_JSON_VALUE]) class WorkBookArticleController( private val readWorkBookArticleUseCase: ReadWorkBookArticleUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readWorkBookArticle( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @PathVariable(value = "workbookId") @Min(value = 1, message = "{min.id}") workbookId: Long, @@ -35,12 +33,7 @@ class WorkBookArticleController( @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } ?: 0L + val memberId = userArgumentDetails.id.toLong() val useCaseOut = ReadWorkBookArticleUseCaseIn( workbookId = workbookId, diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt index 4279a4f1..2d10346a 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt @@ -1,6 +1,5 @@ package com.few.api.web.controller.workbook.article.response -import com.fasterxml.jackson.annotation.JsonFormat import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleOut import com.few.api.web.controller.workbook.response.WriterInfo import java.time.LocalDateTime @@ -12,7 +11,6 @@ data class ReadWorkBookArticleResponse( val content: String, val problemIds: List, val category: String, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val day: Long, ) { diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/BrowseWorkBooksResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/BrowseWorkBooksResponse.kt index 23434471..bedb84de 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/BrowseWorkBooksResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/BrowseWorkBooksResponse.kt @@ -1,6 +1,5 @@ package com.few.api.web.controller.workbook.response -import com.fasterxml.jackson.annotation.JsonFormat import java.net.URL import java.time.LocalDateTime @@ -14,7 +13,6 @@ data class BrowseWorkBookInfo( val title: String, val description: String, val category: String, - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val writers: List, val subscriberCount: Long, diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt index 26e4ff94..090ef090 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt @@ -1,6 +1,5 @@ package com.few.api.web.controller.workbook.response -import com.fasterxml.jackson.annotation.JsonFormat import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseOut import java.net.URL import java.time.LocalDateTime @@ -11,7 +10,6 @@ data class ReadWorkBookResponse( val title: String, val description: String, val category: String, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val writers: List, val articles: List, diff --git a/api/src/main/kotlin/com/few/api/web/support/DayCode.kt b/api/src/main/kotlin/com/few/api/web/support/DayCode.kt new file mode 100644 index 00000000..c2f5ab6b --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/DayCode.kt @@ -0,0 +1,140 @@ +package com.few.api.web.support + +/** + * @see com.few.data.common.code.DayCode + */ +enum class DayCode(val code: String, val days: String) { + MON("0000001", "월"), + TUE("0000010", "화"), + MON_TUE("0000011", "월,화"), + WED("0000100", "수"), + MON_WED("0000101", "월,수"), + TUE_WED("0000110", "화,수"), + MON_TUE_WED("0000111", "월,화,수"), + THU("0001000", "목"), + MON_THU("0001001", "월,목"), + TUE_THU("0001010", "화,목"), + MON_TUE_THU("0001011", "월,화,목"), + WED_THU("0001100", "수,목"), + MON_WED_THU("0001101", "월,수,목"), + TUE_WED_THU("0001110", "화,수,목"), + MON_TUE_WED_THU("0001111", "월,화,수,목"), + FRI("0010000", "금"), + MON_FRI("0010001", "월,금"), + TUE_FRI("0010010", "화,금"), + MON_TUE_FRI("0010011", "월,화,금"), + WED_FRI("0010100", "수,금"), + MON_WED_FRI("0010101", "월,수,금"), + TUE_WED_FRI("0010110", "화,수,금"), + MON_TUE_WED_FRI("0010111", "월,화,수,금"), + THU_FRI("0011000", "목,금"), + MON_THU_FRI("0011001", "월,목,금"), + TUE_THU_FRI("0011010", "화,목,금"), + MON_TUE_THU_FRI("0011011", "월,화,목,금"), + WED_THU_FRI("0011100", "수,목,금"), + MON_WED_THU_FRI("0011101", "월,수,목,금"), + TUE_WED_THU_FRI("0011110", "화,수,목,금"), + MON_TUE_WED_THU_FRI("0011111", "월,화,수,목,금"), + SAT("0100000", "토"), + MON_SAT("0100001", "월,토"), + TUE_SAT("0100010", "화,토"), + MON_TUE_SAT("0100011", "월,화,토"), + WED_SAT("0100100", "수,토"), + MON_WED_SAT("0100101", "월,수,토"), + TUE_WED_SAT("0100110", "화,수,토"), + MON_TUE_WED_SAT("0100111", "월,화,수,토"), + THU_SAT("0101000", "목,토"), + MON_THU_SAT("0101001", "월,목,토"), + TUE_THU_SAT("0101010", "화,목,토"), + MON_TUE_THU_SAT("0101011", "월,화,목,토"), + WED_THU_SAT("0101100", "수,목,토"), + MON_WED_THU_SAT("0101101", "월,수,목,토"), + TUE_WED_THU_SAT("0101110", "화,수,목,토"), + MON_TUE_WED_THU_SAT("0101111", "월,화,수,목,토"), + FRI_SAT("0110000", "금,토"), + MON_FRI_SAT("0110001", "월,금,토"), + TUE_FRI_SAT("0110010", "화,금,토"), + MON_TUE_FRI_SAT("0110011", "월,화,금,토"), + WED_FRI_SAT("0110100", "수,금,토"), + MON_WED_FRI_SAT("0110101", "월,수,금,토"), + TUE_WED_FRI_SAT("0110110", "화,수,금,토"), + MON_TUE_WED_FRI_SAT("0110111", "월,화,수,금,토"), + THU_FRI_SAT("0111000", "목,금,토"), + MON_THU_FRI_SAT("0111001", "월,목,금,토"), + TUE_THU_FRI_SAT("0111010", "화,목,금,토"), + MON_TUE_THU_FRI_SAT("0111011", "월,화,목,금,토"), + WED_THU_FRI_SAT("0111100", "수,목,금,토"), + MON_WED_THU_FRI_SAT("0111101", "월,수,목,금,토"), + TUE_WED_THU_FRI_SAT("0111110", "화,수,목,금,토"), + MON_TUE_WED_THU_FRI_SAT("0111111", "월,화,수,목,금,토"), + SUN("1000000", "일"), + MON_SUN("1000001", "월,일"), + TUE_SUN("1000010", "화,일"), + MON_TUE_SUN("1000011", "월,화,일"), + WED_SUN("1000100", "수,일"), + MON_WED_SUN("1000101", "월,수,일"), + TUE_WED_SUN("1000110", "화,수,일"), + MON_TUE_WED_SUN("1000111", "월,화,수,일"), + THU_SUN("1001000", "목,일"), + MON_THU_SUN("1001001", "월,목,일"), + TUE_THU_SUN("1001010", "화,목,일"), + MON_TUE_THU_SUN("1001011", "월,화,목,일"), + WED_THU_SUN("1001100", "수,목,일"), + MON_WED_THU_SUN("1001101", "월,수,목,일"), + TUE_WED_THU_SUN("1001110", "화,수,목,일"), + MON_TUE_WED_THU_SUN("1001111", "월,화,수,목,일"), + FRI_SUN("1010000", "금,일"), + MON_FRI_SUN("1010001", "월,금,일"), + TUE_FRI_SUN("1010010", "화,금,일"), + MON_TUE_FRI_SUN("1010011", "월,화,금,일"), + WED_FRI_SUN("1010100", "수,금,일"), + MON_WED_FRI_SUN("1010101", "월,수,금,일"), + TUE_WED_FRI_SUN("1010110", "화,수,금,일"), + MON_TUE_WED_FRI_SUN("1010111", "월,화,수,금,일"), + THU_FRI_SUN("1011000", "목,금,일"), + MON_THU_FRI_SUN("1011001", "월,목,금,일"), + TUE_THU_FRI_SUN("1011010", "화,목,금,일"), + MON_TUE_THU_FRI_SUN("1011011", "월,화,목,금,일"), + WED_THU_FRI_SUN("1011100", "수,목,금,일"), + MON_WED_THU_FRI_SUN("1011101", "월,수,목,금,일"), + TUE_WED_THU_FRI_SUN("1011110", "화,수,목,금,일"), + MON_TUE_WED_THU_FRI_SUN("1011111", "월,화,수,목,금,일"), + SAT_SUN("1100000", "토,일"), + MON_SAT_SUN("1100001", "월,토,일"), + TUE_SAT_SUN("1100010", "화,토,일"), + MON_TUE_SAT_SUN("1100011", "월,화,토,일"), + WED_SAT_SUN("1100100", "수,토,일"), + MON_WED_SAT_SUN("1100101", "월,수,토,일"), + TUE_WED_SAT_SUN("1100110", "화,수,토,일"), + MON_TUE_WED_SAT_SUN("1100111", "월,화,수,토,일"), + THU_SAT_SUN("1101000", "목,토,일"), + MON_THU_SAT_SUN("1101001", "월,목,토,일"), + TUE_THU_SAT_SUN("1101010", "화,목,토,일"), + MON_TUE_THU_SAT_SUN("1101011", "월,화,목,토,일"), + WED_THU_SAT_SUN("1101100", "수,목,토,일"), + MON_WED_THU_SAT_SUN("1101101", "월,수,목,토,일"), + TUE_WED_THU_SAT_SUN("1101110", "화,수,목,토,일"), + MON_TUE_WED_THU_SAT_SUN("1101111", "월,화,수,목,토,일"), + FRI_SAT_SUN("1110000", "금,토,일"), + MON_FRI_SAT_SUN("1110001", "월,금,토,일"), + TUE_FRI_SAT_SUN("1110010", "화,금,토,일"), + MON_TUE_FRI_SAT_SUN("1110011", "월,화,금,토,일"), + WED_FRI_SAT_SUN("1110100", "수,금,토,일"), + MON_WED_FRI_SAT_SUN("1110101", "월,수,금,토,일"), + TUE_WED_FRI_SAT_SUN("1110110", "화,수,금,토,일"), + MON_TUE_WED_FRI_SAT_SUN("1110111", "월,화,수,금,토,일"), + THU_FRI_SAT_SUN("1111000", "목,금,토,일"), + MON_THU_FRI_SAT_SUN("1111001", "월,목,금,토,일"), + TUE_THU_FRI_SAT_SUN("1111010", "화,목,금,토,일"), + MON_TUE_THU_FRI_SAT_SUN("1111011", "월,화,목,금,토,일"), + WED_THU_FRI_SAT_SUN("1111100", "수,목,금,토,일"), + MON_WED_THU_FRI_SAT_SUN("1111101", "월,수,목,금,토,일"), + TUE_WED_THU_FRI_SAT_SUN("1111110", "화,수,목,금,토,일"), + MON_TUE_WED_THU_FRI_SAT_SUN("1111111", "월,화,수,목,금,토,일"), + ; + companion object { + fun fromCode(code: String): DayCode { + return entries.first { it.code == code } + } + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/ViewCategory.kt b/api/src/main/kotlin/com/few/api/web/support/ViewCategory.kt index cce04ffd..514cff4e 100644 --- a/api/src/main/kotlin/com/few/api/web/support/ViewCategory.kt +++ b/api/src/main/kotlin/com/few/api/web/support/ViewCategory.kt @@ -2,6 +2,7 @@ package com.few.api.web.support enum class ViewCategory(val viewName: String) { MAIN_CARD("mainCard"), + MY_PAGE("myPage"), ; companion object { diff --git a/api/src/main/kotlin/com/few/api/web/support/WorkBookCategory.kt b/api/src/main/kotlin/com/few/api/web/support/WorkBookCategory.kt index 79620172..ef475999 100644 --- a/api/src/main/kotlin/com/few/api/web/support/WorkBookCategory.kt +++ b/api/src/main/kotlin/com/few/api/web/support/WorkBookCategory.kt @@ -9,6 +9,7 @@ enum class WorkBookCategory(val code: Byte, val parameterName: String, val displ ECONOMY(0, "economy", "경제"), IT(10, "it", "IT"), MARKETING(20, "marketing", "마케팅"), + LANGUAGE(25, "language", "외국어"), CULTURE(30, "culture", "교양"), SCIENCE(40, "science", "과학"), ; diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt new file mode 100644 index 00000000..1fd913a0 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt @@ -0,0 +1,5 @@ +package com.few.api.web.support.method + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class UserArgument \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt new file mode 100644 index 00000000..2dfe9af7 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt @@ -0,0 +1,15 @@ +package com.few.api.web.support.method + +import com.few.api.security.authentication.token.TokenUserDetails +import org.springframework.security.core.GrantedAuthority + +class UserArgumentDetails( + val isAuth: Boolean, + authorities: List, + id: String, + email: String, +) : TokenUserDetails( + authorities = authorities, + id = id, + email = email +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt new file mode 100644 index 00000000..e44f5f55 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt @@ -0,0 +1,59 @@ +package com.few.api.web.support.method + +import com.few.api.security.authentication.authority.AuthorityUtils +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class UserArgumentHandlerMethodArgumentResolver( + private val tokenResolver: TokenResolver, +) : HandlerMethodArgumentResolver { + val log = KotlinLogging.logger {} + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(UserArgument::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): UserArgumentDetails { + val authorization: String? = webRequest.getHeader("Authorization") + + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) + } ?: 0L + + val email = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveEmail(it) + } ?: "" + + val authorities = authorization?.let { + AccessTokenResolver.resolve(it) + }?.let { + tokenResolver.resolveRole(it) + }?.let { + AuthorityUtils.toAuthorities(it) + } ?: emptyList() + + return UserArgumentDetails( + isAuth = authorization != null, + id = memberId.toString(), + email = email, + authorities = authorities + ) + } +} \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt index 035bada6..0a3ffd12 100644 --- a/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt @@ -26,7 +26,8 @@ class CheckProblemUseCaseTest : BehaviorSpec({ given("특정 문제에 대한 정답 확인 요청이 온 상황에서") { val problemId = 1L val submissionVal = "1" - val useCaseIn = CheckProblemUseCaseIn(problemId = problemId, sub = submissionVal) + val useCaseIn = + CheckProblemUseCaseIn(memberId = 0, problemId = problemId, sub = submissionVal) `when`("제출 값과 문제 정답이 같을 경우") { val answer = submissionVal diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt index b7bb374d..11f0794a 100644 --- a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt @@ -2,9 +2,15 @@ package com.few.api.domain.subscription.usecase import com.fasterxml.jackson.databind.ObjectMapper import com.few.api.domain.subscription.service.SubscriptionArticleService +import com.few.api.domain.subscription.service.SubscriptionWorkbookService import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCaseIn +import com.few.api.domain.subscription.usecase.dto.MainCardSubscribeWorkbookDetail +import com.few.api.domain.subscription.usecase.dto.MyPageSubscribeWorkbookDetail import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.record.MemberWorkbookSubscriptionStatusRecord +import com.few.api.repo.dao.subscription.record.SubscriptionSendStatusRecord +import com.few.api.web.support.DayCode +import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookStatus import io.github.oshai.kotlinlogging.KotlinLogging import io.kotest.core.spec.style.BehaviorSpec @@ -12,40 +18,37 @@ import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify +import java.time.LocalTime class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ val log = KotlinLogging.logger {} lateinit var subscriptionDao: SubscriptionDao lateinit var subscriptionArticleService: SubscriptionArticleService + lateinit var subscriptionWorkbookService: SubscriptionWorkbookService lateinit var objectMapper: ObjectMapper lateinit var useCase: BrowseSubscribeWorkbooksUseCase beforeContainer { subscriptionDao = mockk() subscriptionArticleService = mockk() + subscriptionWorkbookService = mockk() objectMapper = mockk() - useCase = BrowseSubscribeWorkbooksUseCase(subscriptionDao, subscriptionArticleService, objectMapper) + useCase = BrowseSubscribeWorkbooksUseCase( + subscriptionDao, + subscriptionArticleService, + subscriptionWorkbookService, + objectMapper + ) } - given("멤버의 구독 워크북 정보 조회 요청이 온 상황에서") { + given("메인 카드 뷰에서 멤버의 구독 워크북 정보를 조회하는 경우") { val memberId = 1L - val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId = memberId) + val useCaseIn = + BrowseSubscribeWorkbooksUseCaseIn(memberId = memberId, view = ViewCategory.MAIN_CARD) `when`("멤버의 구독 워크북 정보가 존재할 경우") { - val inactiveWorkbookId = 1L - val inactiveWorkbookCurrentDay = 2 - val inactiveWorkbookTotalDay = 3 - every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns listOf( - MemberWorkbookSubscriptionStatusRecord( - workbookId = inactiveWorkbookId, - isActiveSub = false, - currentDay = inactiveWorkbookCurrentDay, - totalDay = inactiveWorkbookTotalDay - ) - ) - - val activeWorkbookId = 2L + val activeWorkbookId = 1L val activeWorkbookCurrentDay = 1 val activeWorkbookTotalDay = 3 every { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } returns listOf( @@ -55,7 +58,18 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ currentDay = activeWorkbookCurrentDay, totalDay = activeWorkbookTotalDay ) + ) + val inactiveWorkbookId = 2L + val inactiveWorkbookCurrentDay = 2 + val inactiveWorkbookTotalDay = 3 + every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns listOf( + MemberWorkbookSubscriptionStatusRecord( + workbookId = inactiveWorkbookId, + isActiveSub = false, + currentDay = inactiveWorkbookCurrentDay, + totalDay = inactiveWorkbookTotalDay + ) ) every { @@ -69,21 +83,26 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ activeWorkbookId to activeWorkbookSubscriptionCount ) - every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$inactiveWorkbookId}" andThen "{\"articleId\":$activeWorkbookId}" + every { subscriptionDao.selectAllSubscriptionSendStatus(any()) } returns listOf( + SubscriptionSendStatusRecord( + workbookId = inactiveWorkbookId, + sendTime = LocalTime.of(8, 0), + sendDay = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + ), + SubscriptionSendStatusRecord( + workbookId = activeWorkbookId, + sendTime = LocalTime.of(8, 0), + sendDay = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + ) + ) + + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$activeWorkbookId}" andThen "{\"articleId\":$inactiveWorkbookId}" then("멤버의 구독 워크북 정보를 반환한다") { val useCaseOut = useCase.execute(useCaseIn) useCaseOut.workbooks.size shouldBe 2 - val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] - inActiveSubscriptionWorkbook.workbookId shouldBe inactiveWorkbookId - inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE - inActiveSubscriptionWorkbook.currentDay shouldBe inactiveWorkbookCurrentDay - inActiveSubscriptionWorkbook.totalDay shouldBe inactiveWorkbookTotalDay - inActiveSubscriptionWorkbook.totalSubscriber shouldBe inactiveWorkbookSubscriptionCount - inActiveSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$inactiveWorkbookId}" - - val activeSubscriptionWorkbook = useCaseOut.workbooks[1] + val activeSubscriptionWorkbook = useCaseOut.workbooks[0] as MainCardSubscribeWorkbookDetail activeSubscriptionWorkbook.workbookId shouldBe activeWorkbookId activeSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.ACTIVE activeSubscriptionWorkbook.currentDay shouldBe activeWorkbookCurrentDay @@ -91,10 +110,19 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ activeSubscriptionWorkbook.totalSubscriber shouldBe activeWorkbookSubscriptionCount activeSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$activeWorkbookId}" + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[1] as MainCardSubscribeWorkbookDetail + inActiveSubscriptionWorkbook.workbookId shouldBe inactiveWorkbookId + inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE + inActiveSubscriptionWorkbook.currentDay shouldBe inactiveWorkbookCurrentDay + inActiveSubscriptionWorkbook.totalDay shouldBe inactiveWorkbookTotalDay + inActiveSubscriptionWorkbook.totalSubscriber shouldBe inactiveWorkbookSubscriptionCount + inActiveSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$inactiveWorkbookId}" + verify(exactly = 1) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } - verify(exactly = 2) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { subscriptionDao.selectAllSubscriptionSendStatus(any()) } + verify(exactly = 2) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } verify(exactly = 2) { objectMapper.writeValueAsString(any()) } } } @@ -123,13 +151,21 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ inactiveWorkbookId to inactiveWorkbookSubscriptionCount ) + every { subscriptionDao.selectAllSubscriptionSendStatus(any()) } returns listOf( + SubscriptionSendStatusRecord( + workbookId = inactiveWorkbookId, + sendTime = LocalTime.of(8, 0), + sendDay = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + ) + ) + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$inactiveWorkbookId}" then("멤버의 구독 비활성 워크북 정보를 반환한다") { val useCaseOut = useCase.execute(useCaseIn) useCaseOut.workbooks.size shouldBe 1 - val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] as MainCardSubscribeWorkbookDetail inActiveSubscriptionWorkbook.workbookId shouldBe inactiveWorkbookId inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE inActiveSubscriptionWorkbook.currentDay shouldBe inactiveWorkbookCurrentDay @@ -141,6 +177,7 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { subscriptionDao.selectAllSubscriptionSendStatus(any()) } verify(exactly = 1) { objectMapper.writeValueAsString(any()) } } } @@ -169,13 +206,21 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ activeWorkbookId to activeWorkbookSubscriptionCount ) + every { subscriptionDao.selectAllSubscriptionSendStatus(any()) } returns listOf( + SubscriptionSendStatusRecord( + workbookId = activeWorkbookId, + sendTime = LocalTime.of(8, 0), + sendDay = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + ) + ) + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$activeWorkbookId}" then("멤버의 구독 활성 워크북 정보를 반환한다") { val useCaseOut = useCase.execute(useCaseIn) useCaseOut.workbooks.size shouldBe 1 - val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] as MainCardSubscribeWorkbookDetail inActiveSubscriptionWorkbook.workbookId shouldBe activeWorkbookId inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE inActiveSubscriptionWorkbook.currentDay shouldBe activeWorkbookCurrentDay @@ -185,8 +230,75 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ verify(exactly = 1) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { subscriptionDao.selectAllSubscriptionSendStatus(any()) } verify(exactly = 1) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } + verify(exactly = 1) { objectMapper.writeValueAsString(any()) } + } + } + } + + given("마이 페이지에서 멤버의 구독 워크북 정보를 조회하는 경우") { + val memberId = 1L + val useCaseIn = + BrowseSubscribeWorkbooksUseCaseIn(memberId = memberId, view = ViewCategory.MY_PAGE) + + `when`("멤버의 구독 활성 워크북 정보이 존재할 경우") { + every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns emptyList() + + val activeWorkbookId = 1L + val activeWorkbookCurrentDay = 2 + val activeWorkbookTotalDay = 3 + every { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } returns listOf( + MemberWorkbookSubscriptionStatusRecord( + workbookId = activeWorkbookId, + isActiveSub = false, + currentDay = activeWorkbookCurrentDay, + totalDay = activeWorkbookTotalDay + ) + ) + + every { + subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) + } returns activeWorkbookId + + val activeWorkbookSubscriptionCount = 1 + every { subscriptionDao.countAllWorkbookSubscription(any()) } returns mapOf( + activeWorkbookId to activeWorkbookSubscriptionCount + ) + + every { subscriptionDao.selectAllSubscriptionSendStatus(any()) } returns listOf( + SubscriptionSendStatusRecord( + workbookId = activeWorkbookId, + sendTime = LocalTime.of(8, 0), + sendDay = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + ) + ) + + every { subscriptionWorkbookService.readAllWorkbookTitle(any()) } returns mapOf( + activeWorkbookId to "title" + ) + + every { objectMapper.writeValueAsString(any()) } returns "{\"id\":$activeWorkbookId, \"title\":\"title\"}" + + then("멤버의 구독 워크북 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe 1 + + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] as MyPageSubscribeWorkbookDetail + inActiveSubscriptionWorkbook.workbookId shouldBe activeWorkbookId + inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE + inActiveSubscriptionWorkbook.currentDay shouldBe activeWorkbookCurrentDay + inActiveSubscriptionWorkbook.totalDay shouldBe activeWorkbookTotalDay + inActiveSubscriptionWorkbook.totalSubscriber shouldBe activeWorkbookSubscriptionCount + inActiveSubscriptionWorkbook.workbookInfo shouldBe "{\"id\":$activeWorkbookId, \"title\":\"title\"}" + + verify(exactly = 0) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { subscriptionDao.selectAllSubscriptionSendStatus(any()) } + verify(exactly = 0) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } + verify(exactly = 1) { subscriptionWorkbookService.readAllWorkbookTitle(any()) } verify(exactly = 1) { objectMapper.writeValueAsString(any()) } } } 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 afaed095..dbfae170 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 @@ -156,11 +156,12 @@ class ProblemControllerTest : ControllerTestSpec() { val api = "CheckProblem" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/{problemId}").build().toUriString() + val memberId = 0L val problemId = 1L val sub = "제출답" val body = objectMapper.writeValueAsString(CheckProblemRequest(sub = sub)) - val useCaseIn = CheckProblemUseCaseIn(problemId, sub = sub) + val useCaseIn = CheckProblemUseCaseIn(memberId, problemId, sub) val useCaseOut = CheckProblemUseCaseOut( explanation = "ETF는 일반적으로 낮은 운용 비용을 특징으로 합니다.이는 ETF가 보통 지수 추종(passive management) 방식으로 운용되기 때문입니다. 지수를 추종하는 전략은 액티브 매니지먼트(active management)에 비해 관리가 덜 복잡하고, 따라서 비용이 낮습니다.", answer = "2", diff --git a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt index bffabac2..b8136735 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt @@ -9,6 +9,9 @@ import com.few.api.web.controller.description.Description import com.few.api.web.controller.subscription.request.UnsubscribeWorkbookRequest import com.few.api.domain.subscription.usecase.dto.* import com.few.api.web.controller.helper.* +import com.few.api.web.controller.subscription.request.UpdateSubscriptionDayRequest +import com.few.api.web.controller.subscription.request.UpdateSubscriptionTimeRequest +import com.few.api.web.support.DayCode import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookStatus import org.junit.jupiter.api.DisplayName @@ -17,12 +20,12 @@ import org.mockito.Mockito.doNothing import org.mockito.Mockito.doReturn import org.springframework.http.MediaType import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document -import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get -import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.* import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.security.test.context.support.WithUserDetails import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.web.util.UriComponentsBuilder +import java.time.LocalTime class SubscriptionControllerTest : ControllerTestSpec() { @@ -32,49 +35,53 @@ class SubscriptionControllerTest : ControllerTestSpec() { } @Test - @DisplayName("[GET] /api/v1/subscriptions/workbooks") + @DisplayName("[GET] /api/v1/subscriptions/workbooks?view=mainCard") @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") - fun browseSubscribeWorkbooks() { + fun browseSubscribeWorkbooksViewMainCard() { // given - val api = "BrowseSubscribeWorkBooks" + val api = "BrowseSubscribeWorkBooksViewMainCard" val token = "thisisaccesstoken" val view = ViewCategory.MAIN_CARD val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/subscriptions/workbooks") - .queryParam("view", view) + .queryParam("view", view.viewName) .build() .toUriString() val memberId = 1L - val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId) + val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId, view) val useCaseOut = BrowseSubscribeWorkbooksUseCaseOut( + clazz = MainCardSubscribeWorkbookDetail::class.java, workbooks = listOf( - SubscribeWorkbookDetail( + MainCardSubscribeWorkbookDetail( workbookId = 1L, isActiveSub = WorkBookStatus.ACTIVE, currentDay = 1, totalDay = 3, rank = 0, totalSubscriber = 100, + subscription = Subscription(), articleInfo = "{\"articleId\":1}" ), - SubscribeWorkbookDetail( + MainCardSubscribeWorkbookDetail( workbookId = 2L, isActiveSub = WorkBookStatus.ACTIVE, currentDay = 2, totalDay = 3, rank = 0, totalSubscriber = 1, + subscription = Subscription(), articleInfo = "{\"articleId\":5}" ), - SubscribeWorkbookDetail( + MainCardSubscribeWorkbookDetail( workbookId = 3L, isActiveSub = WorkBookStatus.DONE, currentDay = 3, totalDay = 3, rank = 0, totalSubscriber = 2, - articleInfo = "{}" + subscription = Subscription(), + articleInfo = "{\"articleId\":6}" ) ) ) @@ -91,7 +98,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() - .description("구독한 학습지 정보 목록을 조회합니다.") + .description("메인 카드에 표시할 구독한 학습지 정보 목록을 조회합니다.") .summary(api.toIdentifier()) .privateResource(false) .deprecated(false) @@ -123,7 +130,115 @@ class SubscriptionControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.workbooks[].totalSubscriber") .fieldWithNumber("누적 구독자 수"), PayloadDocumentation.fieldWithPath("data.workbooks[].articleInfo") - .fieldWithString("학습지 정보(Json 타입)") + .fieldWithString("아티클 정보(Json 타입)"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription") + .fieldWithObject("구독 정보"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription.time") + .fieldWithString("구독 시간"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription.dateTimeCode") + .fieldWithString("구독 시간 코드") + ) + ) + ) + .build() + ) + ) + ) + } + + @Test + @DisplayName("[GET] /api/v1/subscriptions/workbooks?view=myPage") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + fun browseSubscribeWorkbooksViewMyPage() { + // given + val api = "BrowseSubscribeWorkBooksViewMyPage" + val token = "thisisaccesstoken" + val view = ViewCategory.MY_PAGE + val uri = UriComponentsBuilder.newInstance() + .path("$BASE_URL/subscriptions/workbooks") + .queryParam("view", view.viewName) + .build() + .toUriString() + + val memberId = 1L + val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId, view) + val useCaseOut = BrowseSubscribeWorkbooksUseCaseOut( + clazz = MyPageSubscribeWorkbookDetail::class.java, + workbooks = listOf( + MyPageSubscribeWorkbookDetail( + workbookId = 1L, + isActiveSub = WorkBookStatus.ACTIVE, + currentDay = 1, + totalDay = 3, + rank = 0, + totalSubscriber = 100, + subscription = Subscription(), + workbookInfo = "{\"id\":1, \"title\":\"title1\"}" + ), + MyPageSubscribeWorkbookDetail( + workbookId = 2L, + isActiveSub = WorkBookStatus.ACTIVE, + currentDay = 2, + totalDay = 3, + rank = 0, + totalSubscriber = 1, + subscription = Subscription(), + workbookInfo = "{\"id\":2, \"title\":\"title2\"}" + ) + ) + ) + doReturn(useCaseOut).`when`(browseSubscribeWorkbooksUseCase).execute(useCaseIn) + + // when + mockMvc.perform( + get(uri) + .header("Authorization", "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( + api.toIdentifier(), + ResourceDocumentation.resource( + ResourceSnippetParameters.builder() + .description("마이 페이지에 표시할 구독한 학습지 정보 목록을 조회합니다.") + .summary(api.toIdentifier()) + .privateResource(false) + .deprecated(false) + .tag(TAG) + .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) + .responseSchema(Schema.schema(api.toResponseSchema())) + .responseFields( + *Description.describe( + arrayOf( + PayloadDocumentation.fieldWithPath("data") + .fieldWithObject("data"), + PayloadDocumentation.fieldWithPath("data.workbooks") + .fieldWithArray("학습지 목록"), + PayloadDocumentation.fieldWithPath("data.workbooks[].id") + .fieldWithNumber("학습지 Id"), + PayloadDocumentation.fieldWithPath("data.workbooks[].status") + .fieldWithString("구독 상태"), + PayloadDocumentation.fieldWithPath("data.workbooks[].totalDay") + .fieldWithNumber("총 일수"), + PayloadDocumentation.fieldWithPath("data.workbooks[].currentDay") + .fieldWithNumber("현재 일수"), + PayloadDocumentation.fieldWithPath("data.workbooks[].rank") + .fieldWithNumber("순위"), + PayloadDocumentation.fieldWithPath("data.workbooks[].totalSubscriber") + .fieldWithNumber("누적 구독자 수"), + PayloadDocumentation.fieldWithPath("data.workbooks[].workbookInfo") + .fieldWithString("학습지 정보(Json 타입)"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription") + .fieldWithObject("구독 정보"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription.time") + .fieldWithString("구독 시간"), + PayloadDocumentation.fieldWithPath("data.workbooks[].subscription.dateTimeCode") + .fieldWithString("구독 시간 코드") ) ) ) @@ -297,4 +412,112 @@ class SubscriptionControllerTest : ControllerTestSpec() { ) ) } + + @Test + @DisplayName("[PATCH] /api/v1/subscriptions/time") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + fun updateSubscriptionTime() { + // given + val api = "UpdateSubscriptionTime" + val token = "thisisaccesstoken" + val uri = UriComponentsBuilder.newInstance() + .path("$BASE_URL/subscriptions/time") + .build() + .toUriString() + + val time = LocalTime.of(8, 0) + val workbookId = 1L + val body = objectMapper.writeValueAsString( + UpdateSubscriptionTimeRequest( + time = time, + workbookId = workbookId + ) + ) + + // when + mockMvc.perform( + patch(uri) + .header("Authorization", "Bearer $token") + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( + api.toIdentifier(), + ResourceDocumentation.resource( + ResourceSnippetParameters.builder() + .description("구독 시간을 변경합니다.") + .summary(api.toIdentifier()) + .privateResource(false) + .deprecated(false) + .tag(TAG) + .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) + .responseSchema(Schema.schema(api.toResponseSchema())) + .responseFields( + *Description.describe() + ) + .build() + ) + ) + ) + } + + @Test + @DisplayName("[PATCH] /api/v1/subscriptions/day") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") + fun updateSubscriptionDay() { + // given + val api = "UpdateSubscriptionDay" + val token = "thisisaccesstoken" + val uri = UriComponentsBuilder.newInstance() + .path("$BASE_URL/subscriptions/day") + .build() + .toUriString() + + val dateTimeCode = DayCode.MON_TUE_WED_THU_FRI_SAT_SUN + val workbookId = 1L + val body = objectMapper.writeValueAsString( + UpdateSubscriptionDayRequest( + workbookId = workbookId, + dayCode = dateTimeCode.code + ) + ) + + // when + mockMvc.perform( + patch(uri) + .header("Authorization", "Bearer $token") + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( + api.toIdentifier(), + ResourceDocumentation.resource( + ResourceSnippetParameters.builder() + .description("구독 요일을 변경합니다.") + .summary(api.toIdentifier()) + .privateResource(false) + .deprecated(false) + .tag(TAG) + .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) + .responseSchema(Schema.schema(api.toResponseSchema())) + .responseFields( + *Description.describe() + ) + .build() + ) + ) + ) + } } \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/data/common/code/BatchDayCode.kt b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchDayCode.kt new file mode 100644 index 00000000..f9871b23 --- /dev/null +++ b/batch/src/main/kotlin/com/few/batch/data/common/code/BatchDayCode.kt @@ -0,0 +1,140 @@ +package com.few.batch.data.common.code + +/** + * @see com.few.data.common.code.DayCode + */ +enum class BatchDayCode(val code: String, val days: String) { + MON("0000001", "월"), + TUE("0000010", "화"), + MON_TUE("0000011", "월,화"), + WED("0000100", "수"), + MON_WED("0000101", "월,수"), + TUE_WED("0000110", "화,수"), + MON_TUE_WED("0000111", "월,화,수"), + THU("0001000", "목"), + MON_THU("0001001", "월,목"), + TUE_THU("0001010", "화,목"), + MON_TUE_THU("0001011", "월,화,목"), + WED_THU("0001100", "수,목"), + MON_WED_THU("0001101", "월,수,목"), + TUE_WED_THU("0001110", "화,수,목"), + MON_TUE_WED_THU("0001111", "월,화,수,목"), + FRI("0010000", "금"), + MON_FRI("0010001", "월,금"), + TUE_FRI("0010010", "화,금"), + MON_TUE_FRI("0010011", "월,화,금"), + WED_FRI("0010100", "수,금"), + MON_WED_FRI("0010101", "월,수,금"), + TUE_WED_FRI("0010110", "화,수,금"), + MON_TUE_WED_FRI("0010111", "월,화,수,금"), + THU_FRI("0011000", "목,금"), + MON_THU_FRI("0011001", "월,목,금"), + TUE_THU_FRI("0011010", "화,목,금"), + MON_TUE_THU_FRI("0011011", "월,화,목,금"), + WED_THU_FRI("0011100", "수,목,금"), + MON_WED_THU_FRI("0011101", "월,수,목,금"), + TUE_WED_THU_FRI("0011110", "화,수,목,금"), + MON_TUE_WED_THU_FRI("0011111", "월,화,수,목,금"), + SAT("0100000", "토"), + MON_SAT("0100001", "월,토"), + TUE_SAT("0100010", "화,토"), + MON_TUE_SAT("0100011", "월,화,토"), + WED_SAT("0100100", "수,토"), + MON_WED_SAT("0100101", "월,수,토"), + TUE_WED_SAT("0100110", "화,수,토"), + MON_TUE_WED_SAT("0100111", "월,화,수,토"), + THU_SAT("0101000", "목,토"), + MON_THU_SAT("0101001", "월,목,토"), + TUE_THU_SAT("0101010", "화,목,토"), + MON_TUE_THU_SAT("0101011", "월,화,목,토"), + WED_THU_SAT("0101100", "수,목,토"), + MON_WED_THU_SAT("0101101", "월,수,목,토"), + TUE_WED_THU_SAT("0101110", "화,수,목,토"), + MON_TUE_WED_THU_SAT("0101111", "월,화,수,목,토"), + FRI_SAT("0110000", "금,토"), + MON_FRI_SAT("0110001", "월,금,토"), + TUE_FRI_SAT("0110010", "화,금,토"), + MON_TUE_FRI_SAT("0110011", "월,화,금,토"), + WED_FRI_SAT("0110100", "수,금,토"), + MON_WED_FRI_SAT("0110101", "월,수,금,토"), + TUE_WED_FRI_SAT("0110110", "화,수,금,토"), + MON_TUE_WED_FRI_SAT("0110111", "월,화,수,금,토"), + THU_FRI_SAT("0111000", "목,금,토"), + MON_THU_FRI_SAT("0111001", "월,목,금,토"), + TUE_THU_FRI_SAT("0111010", "화,목,금,토"), + MON_TUE_THU_FRI_SAT("0111011", "월,화,목,금,토"), + WED_THU_FRI_SAT("0111100", "수,목,금,토"), + MON_WED_THU_FRI_SAT("0111101", "월,수,목,금,토"), + TUE_WED_THU_FRI_SAT("0111110", "화,수,목,금,토"), + MON_TUE_WED_THU_FRI_SAT("0111111", "월,화,수,목,금,토"), + SUN("1000000", "일"), + MON_SUN("1000001", "월,일"), + TUE_SUN("1000010", "화,일"), + MON_TUE_SUN("1000011", "월,화,일"), + WED_SUN("1000100", "수,일"), + MON_WED_SUN("1000101", "월,수,일"), + TUE_WED_SUN("1000110", "화,수,일"), + MON_TUE_WED_SUN("1000111", "월,화,수,일"), + THU_SUN("1001000", "목,일"), + MON_THU_SUN("1001001", "월,목,일"), + TUE_THU_SUN("1001010", "화,목,일"), + MON_TUE_THU_SUN("1001011", "월,화,목,일"), + WED_THU_SUN("1001100", "수,목,일"), + MON_WED_THU_SUN("1001101", "월,수,목,일"), + TUE_WED_THU_SUN("1001110", "화,수,목,일"), + MON_TUE_WED_THU_SUN("1001111", "월,화,수,목,일"), + FRI_SUN("1010000", "금,일"), + MON_FRI_SUN("1010001", "월,금,일"), + TUE_FRI_SUN("1010010", "화,금,일"), + MON_TUE_FRI_SUN("1010011", "월,화,금,일"), + WED_FRI_SUN("1010100", "수,금,일"), + MON_WED_FRI_SUN("1010101", "월,수,금,일"), + TUE_WED_FRI_SUN("1010110", "화,수,금,일"), + MON_TUE_WED_FRI_SUN("1010111", "월,화,수,금,일"), + THU_FRI_SUN("1011000", "목,금,일"), + MON_THU_FRI_SUN("1011001", "월,목,금,일"), + TUE_THU_FRI_SUN("1011010", "화,목,금,일"), + MON_TUE_THU_FRI_SUN("1011011", "월,화,목,금,일"), + WED_THU_FRI_SUN("1011100", "수,목,금,일"), + MON_WED_THU_FRI_SUN("1011101", "월,수,목,금,일"), + TUE_WED_THU_FRI_SUN("1011110", "화,수,목,금,일"), + MON_TUE_WED_THU_FRI_SUN("1011111", "월,화,수,목,금,일"), + SAT_SUN("1100000", "토,일"), + MON_SAT_SUN("1100001", "월,토,일"), + TUE_SAT_SUN("1100010", "화,토,일"), + MON_TUE_SAT_SUN("1100011", "월,화,토,일"), + WED_SAT_SUN("1100100", "수,토,일"), + MON_WED_SAT_SUN("1100101", "월,수,토,일"), + TUE_WED_SAT_SUN("1100110", "화,수,토,일"), + MON_TUE_WED_SAT_SUN("1100111", "월,화,수,토,일"), + THU_SAT_SUN("1101000", "목,토,일"), + MON_THU_SAT_SUN("1101001", "월,목,토,일"), + TUE_THU_SAT_SUN("1101010", "화,목,토,일"), + MON_TUE_THU_SAT_SUN("1101011", "월,화,목,토,일"), + WED_THU_SAT_SUN("1101100", "수,목,토,일"), + MON_WED_THU_SAT_SUN("1101101", "월,수,목,토,일"), + TUE_WED_THU_SAT_SUN("1101110", "화,수,목,토,일"), + MON_TUE_WED_THU_SAT_SUN("1101111", "월,화,수,목,토,일"), + FRI_SAT_SUN("1110000", "금,토,일"), + MON_FRI_SAT_SUN("1110001", "월,금,토,일"), + TUE_FRI_SAT_SUN("1110010", "화,금,토,일"), + MON_TUE_FRI_SAT_SUN("1110011", "월,화,금,토,일"), + WED_FRI_SAT_SUN("1110100", "수,금,토,일"), + MON_WED_FRI_SAT_SUN("1110101", "월,수,금,토,일"), + TUE_WED_FRI_SAT_SUN("1110110", "화,수,금,토,일"), + MON_TUE_WED_FRI_SAT_SUN("1110111", "월,화,수,금,토,일"), + THU_FRI_SAT_SUN("1111000", "목,금,토,일"), + MON_THU_FRI_SAT_SUN("1111001", "월,목,금,토,일"), + TUE_THU_FRI_SAT_SUN("1111010", "화,목,금,토,일"), + MON_TUE_THU_FRI_SAT_SUN("1111011", "월,화,목,금,토,일"), + WED_THU_FRI_SAT_SUN("1111100", "수,목,금,토,일"), + MON_WED_THU_FRI_SAT_SUN("1111101", "월,수,목,금,토,일"), + TUE_WED_THU_FRI_SAT_SUN("1111110", "화,수,목,금,토,일"), + MON_TUE_WED_THU_FRI_SAT_SUN("1111111", "월,화,수,목,금,토,일"), + ; + companion object { + fun fromCode(code: String): BatchDayCode { + return entries.first { it.code == code } + } + } +} \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/reader/WorkBookSubscriberReader.kt b/batch/src/main/kotlin/com/few/batch/service/article/reader/WorkBookSubscriberReader.kt index 11ebe7d8..ae25a8b4 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/reader/WorkBookSubscriberReader.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/reader/WorkBookSubscriberReader.kt @@ -1,10 +1,15 @@ package com.few.batch.service.article.reader +import com.few.batch.data.common.code.BatchDayCode import com.few.batch.service.article.dto.WorkBookSubscriberItem -import jooq.jooq_dsl.tables.Subscription +import jooq.jooq_dsl.tables.Subscription.SUBSCRIPTION import org.jooq.DSLContext import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId @Component class WorkBookSubscriberReader( @@ -14,16 +19,25 @@ class WorkBookSubscriberReader( /** 구독 테이블에서 학습지를 구독하고 있는 회원의 정보를 조회한다.*/ @Transactional(readOnly = true) fun execute(): List { - val subscriptionT = Subscription.SUBSCRIPTION + val time = LocalTime.now(ZoneId.of("Asia/Seoul")).hour.let { LocalTime.of(it, 0, 0) } + val date = LocalDate.now(ZoneId.of("Asia/Seoul")) + val sendDay = + if ((date.dayOfWeek == DayOfWeek.SATURDAY) || (date.dayOfWeek == DayOfWeek.SUNDAY)) { + BatchDayCode.MON_TUE_WED_THU_FRI_SAT_SUN.code + } else { + BatchDayCode.MON_TUE_WED_THU_FRI.code + } return dslContext.select( - subscriptionT.MEMBER_ID.`as`(WorkBookSubscriberItem::memberId.name), - subscriptionT.TARGET_WORKBOOK_ID.`as`(WorkBookSubscriberItem::targetWorkBookId.name), - subscriptionT.PROGRESS.`as`(WorkBookSubscriberItem::progress.name) + SUBSCRIPTION.MEMBER_ID.`as`(WorkBookSubscriberItem::memberId.name), + SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(WorkBookSubscriberItem::targetWorkBookId.name), + SUBSCRIPTION.PROGRESS.`as`(WorkBookSubscriberItem::progress.name) ) - .from(subscriptionT) - .where(subscriptionT.TARGET_MEMBER_ID.isNull) - .and(subscriptionT.DELETED_AT.isNull) + .from(SUBSCRIPTION) + .where(SUBSCRIPTION.SEND_TIME.eq(time)) + .and(SUBSCRIPTION.SEND_DAY.eq(sendDay)) + .and(SUBSCRIPTION.TARGET_MEMBER_ID.isNull) + .and(SUBSCRIPTION.DELETED_AT.isNull) .fetchInto(WorkBookSubscriberItem::class.java) } } \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt index 3b3f2ce6..b417cff1 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/service/BrowseMemberEmailService.kt @@ -16,6 +16,7 @@ class BrowseMemberEmailService( ) .from(Member.MEMBER) .where(Member.MEMBER.ID.`in`(memberIds)) + .and(Member.MEMBER.DELETED_AT.isNull) .fetch() .intoMap(Member.MEMBER.ID, Member.MEMBER.EMAIL) } diff --git a/data/db/migration/entity/V1.00.0.21__add_subscription_time.sql b/data/db/migration/entity/V1.00.0.21__add_subscription_time.sql new file mode 100644 index 00000000..e12fe9d9 --- /dev/null +++ b/data/db/migration/entity/V1.00.0.21__add_subscription_time.sql @@ -0,0 +1,2 @@ +-- 구독 전송 시간을 추가한다. +ALTER TABLE SUBSCRIPTION ADD COLUMN send_time TIME DEFAULT '08:00'; diff --git a/data/db/migration/entity/V1.00.0.22__add_subscription_day.sql b/data/db/migration/entity/V1.00.0.22__add_subscription_day.sql new file mode 100644 index 00000000..0d7ff59f --- /dev/null +++ b/data/db/migration/entity/V1.00.0.22__add_subscription_day.sql @@ -0,0 +1,2 @@ +-- 구독 전송 요일 추가 +ALTER TABLE SUBSCRIPTION ADD COLUMN send_day CHAR(10) NOT NULL DEFAULT '0011111'; diff --git a/data/db/migration/entity/V1.00.0.23__add_subscription_modified_at.sql b/data/db/migration/entity/V1.00.0.23__add_subscription_modified_at.sql new file mode 100644 index 00000000..1ba732aa --- /dev/null +++ b/data/db/migration/entity/V1.00.0.23__add_subscription_modified_at.sql @@ -0,0 +1,2 @@ +-- 구독 정보 수정 시간을 추가합니다. +ALTER TABLE SUBSCRIPTION ADD COLUMN modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/data/src/main/kotlin/com/few/data/common/code/CategoryType.kt b/data/src/main/kotlin/com/few/data/common/code/CategoryType.kt index 60838cda..04b123b2 100644 --- a/data/src/main/kotlin/com/few/data/common/code/CategoryType.kt +++ b/data/src/main/kotlin/com/few/data/common/code/CategoryType.kt @@ -2,12 +2,14 @@ package com.few.data.common.code /** * @see com.few.batch.data.common.code.BatchCategoryType + * @see com.few.api.web.support.WorkBookCategory */ enum class CategoryType(val code: Byte, val displayName: String) { All(-1, "전체"), // Should not be stored in the DB ECONOMY(0, "경제"), IT(10, "IT"), MARKETING(20, "마케팅"), + LANGUAGE(25, "외국어"), CULTURE(30, "교양"), SCIENCE(40, "과학"), ; diff --git a/data/src/main/kotlin/com/few/data/common/code/DayCode.kt b/data/src/main/kotlin/com/few/data/common/code/DayCode.kt new file mode 100644 index 00000000..ff2da17f --- /dev/null +++ b/data/src/main/kotlin/com/few/data/common/code/DayCode.kt @@ -0,0 +1,141 @@ +package com.few.data.common.code + +/** + * @see com.few.api.web.support.DayCode + * @see com.few.batch.data.common.code.BatchDayCode + */ +enum class DayCode(val code: String, val days: String) { + MON("0000001", "월"), + TUE("0000010", "화"), + MON_TUE("0000011", "월,화"), + WED("0000100", "수"), + MON_WED("0000101", "월,수"), + TUE_WED("0000110", "화,수"), + MON_TUE_WED("0000111", "월,화,수"), + THU("0001000", "목"), + MON_THU("0001001", "월,목"), + TUE_THU("0001010", "화,목"), + MON_TUE_THU("0001011", "월,화,목"), + WED_THU("0001100", "수,목"), + MON_WED_THU("0001101", "월,수,목"), + TUE_WED_THU("0001110", "화,수,목"), + MON_TUE_WED_THU("0001111", "월,화,수,목"), + FRI("0010000", "금"), + MON_FRI("0010001", "월,금"), + TUE_FRI("0010010", "화,금"), + MON_TUE_FRI("0010011", "월,화,금"), + WED_FRI("0010100", "수,금"), + MON_WED_FRI("0010101", "월,수,금"), + TUE_WED_FRI("0010110", "화,수,금"), + MON_TUE_WED_FRI("0010111", "월,화,수,금"), + THU_FRI("0011000", "목,금"), + MON_THU_FRI("0011001", "월,목,금"), + TUE_THU_FRI("0011010", "화,목,금"), + MON_TUE_THU_FRI("0011011", "월,화,목,금"), + WED_THU_FRI("0011100", "수,목,금"), + MON_WED_THU_FRI("0011101", "월,수,목,금"), + TUE_WED_THU_FRI("0011110", "화,수,목,금"), + MON_TUE_WED_THU_FRI("0011111", "월,화,수,목,금"), + SAT("0100000", "토"), + MON_SAT("0100001", "월,토"), + TUE_SAT("0100010", "화,토"), + MON_TUE_SAT("0100011", "월,화,토"), + WED_SAT("0100100", "수,토"), + MON_WED_SAT("0100101", "월,수,토"), + TUE_WED_SAT("0100110", "화,수,토"), + MON_TUE_WED_SAT("0100111", "월,화,수,토"), + THU_SAT("0101000", "목,토"), + MON_THU_SAT("0101001", "월,목,토"), + TUE_THU_SAT("0101010", "화,목,토"), + MON_TUE_THU_SAT("0101011", "월,화,목,토"), + WED_THU_SAT("0101100", "수,목,토"), + MON_WED_THU_SAT("0101101", "월,수,목,토"), + TUE_WED_THU_SAT("0101110", "화,수,목,토"), + MON_TUE_WED_THU_SAT("0101111", "월,화,수,목,토"), + FRI_SAT("0110000", "금,토"), + MON_FRI_SAT("0110001", "월,금,토"), + TUE_FRI_SAT("0110010", "화,금,토"), + MON_TUE_FRI_SAT("0110011", "월,화,금,토"), + WED_FRI_SAT("0110100", "수,금,토"), + MON_WED_FRI_SAT("0110101", "월,수,금,토"), + TUE_WED_FRI_SAT("0110110", "화,수,금,토"), + MON_TUE_WED_FRI_SAT("0110111", "월,화,수,금,토"), + THU_FRI_SAT("0111000", "목,금,토"), + MON_THU_FRI_SAT("0111001", "월,목,금,토"), + TUE_THU_FRI_SAT("0111010", "화,목,금,토"), + MON_TUE_THU_FRI_SAT("0111011", "월,화,목,금,토"), + WED_THU_FRI_SAT("0111100", "수,목,금,토"), + MON_WED_THU_FRI_SAT("0111101", "월,수,목,금,토"), + TUE_WED_THU_FRI_SAT("0111110", "화,수,목,금,토"), + MON_TUE_WED_THU_FRI_SAT("0111111", "월,화,수,목,금,토"), + SUN("1000000", "일"), + MON_SUN("1000001", "월,일"), + TUE_SUN("1000010", "화,일"), + MON_TUE_SUN("1000011", "월,화,일"), + WED_SUN("1000100", "수,일"), + MON_WED_SUN("1000101", "월,수,일"), + TUE_WED_SUN("1000110", "화,수,일"), + MON_TUE_WED_SUN("1000111", "월,화,수,일"), + THU_SUN("1001000", "목,일"), + MON_THU_SUN("1001001", "월,목,일"), + TUE_THU_SUN("1001010", "화,목,일"), + MON_TUE_THU_SUN("1001011", "월,화,목,일"), + WED_THU_SUN("1001100", "수,목,일"), + MON_WED_THU_SUN("1001101", "월,수,목,일"), + TUE_WED_THU_SUN("1001110", "화,수,목,일"), + MON_TUE_WED_THU_SUN("1001111", "월,화,수,목,일"), + FRI_SUN("1010000", "금,일"), + MON_FRI_SUN("1010001", "월,금,일"), + TUE_FRI_SUN("1010010", "화,금,일"), + MON_TUE_FRI_SUN("1010011", "월,화,금,일"), + WED_FRI_SUN("1010100", "수,금,일"), + MON_WED_FRI_SUN("1010101", "월,수,금,일"), + TUE_WED_FRI_SUN("1010110", "화,수,금,일"), + MON_TUE_WED_FRI_SUN("1010111", "월,화,수,금,일"), + THU_FRI_SUN("1011000", "목,금,일"), + MON_THU_FRI_SUN("1011001", "월,목,금,일"), + TUE_THU_FRI_SUN("1011010", "화,목,금,일"), + MON_TUE_THU_FRI_SUN("1011011", "월,화,목,금,일"), + WED_THU_FRI_SUN("1011100", "수,목,금,일"), + MON_WED_THU_FRI_SUN("1011101", "월,수,목,금,일"), + TUE_WED_THU_FRI_SUN("1011110", "화,수,목,금,일"), + MON_TUE_WED_THU_FRI_SUN("1011111", "월,화,수,목,금,일"), + SAT_SUN("1100000", "토,일"), + MON_SAT_SUN("1100001", "월,토,일"), + TUE_SAT_SUN("1100010", "화,토,일"), + MON_TUE_SAT_SUN("1100011", "월,화,토,일"), + WED_SAT_SUN("1100100", "수,토,일"), + MON_WED_SAT_SUN("1100101", "월,수,토,일"), + TUE_WED_SAT_SUN("1100110", "화,수,토,일"), + MON_TUE_WED_SAT_SUN("1100111", "월,화,수,토,일"), + THU_SAT_SUN("1101000", "목,토,일"), + MON_THU_SAT_SUN("1101001", "월,목,토,일"), + TUE_THU_SAT_SUN("1101010", "화,목,토,일"), + MON_TUE_THU_SAT_SUN("1101011", "월,화,목,토,일"), + WED_THU_SAT_SUN("1101100", "수,목,토,일"), + MON_WED_THU_SAT_SUN("1101101", "월,수,목,토,일"), + TUE_WED_THU_SAT_SUN("1101110", "화,수,목,토,일"), + MON_TUE_WED_THU_SAT_SUN("1101111", "월,화,수,목,토,일"), + FRI_SAT_SUN("1110000", "금,토,일"), + MON_FRI_SAT_SUN("1110001", "월,금,토,일"), + TUE_FRI_SAT_SUN("1110010", "화,금,토,일"), + MON_TUE_FRI_SAT_SUN("1110011", "월,화,금,토,일"), + WED_FRI_SAT_SUN("1110100", "수,금,토,일"), + MON_WED_FRI_SAT_SUN("1110101", "월,수,금,토,일"), + TUE_WED_FRI_SAT_SUN("1110110", "화,수,금,토,일"), + MON_TUE_WED_FRI_SAT_SUN("1110111", "월,화,수,금,토,일"), + THU_FRI_SAT_SUN("1111000", "목,금,토,일"), + MON_THU_FRI_SAT_SUN("1111001", "월,목,금,토,일"), + TUE_THU_FRI_SAT_SUN("1111010", "화,목,금,토,일"), + MON_TUE_THU_FRI_SAT_SUN("1111011", "월,화,목,금,토,일"), + WED_THU_FRI_SAT_SUN("1111100", "수,목,금,토,일"), + MON_WED_THU_FRI_SAT_SUN("1111101", "월,수,목,금,토,일"), + TUE_WED_THU_FRI_SAT_SUN("1111110", "화,수,목,금,토,일"), + MON_TUE_WED_THU_FRI_SAT_SUN("1111111", "월,화,수,목,금,토,일"), + ; + companion object { + fun fromCode(code: String): DayCode { + return entries.first { it.code == code } + } + } +} \ No newline at end of file