From b27388429d7553e6eb2322f570cfec0ca70a5777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=ED=9B=88?= Date: Sun, 4 Aug 2024 17:04:42 +0900 Subject: [PATCH] =?UTF-8?q?[Fix/#263]=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0:=20insert=20=EC=BF=BC=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20-=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EA=B0=84=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B2=BD=ED=95=A9=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: workbook record, dto non-nullable 필드로 수정 * fix: article main card의 workbook 컬럼 default 타입 변경 (JSON_OBJECT -> JSON_ARRAY) * fix: article_main_card Insert(workbook제외), Update(workbook만) 쿼리 추가 * feat: add new funcation fromName in CategoryType * feat: member 조회시 작가일 경우 name도 가져오도록 추가 * feat: 아티클 신규 저장 시, article_main_card(workbook 제외) 저장하도록 반영 * refactor: 필드명 수정 * feat: workbook-article 연결시 article_main_card의 workbook 컬럼 업데이트 하도록 변경 * fix: remove V1.00.0.16__alter_article_main_card_table.sql * chore: 테스트용 임시 주석 처리 * refactor: 아티클 카테고리 조회 부분 리펙토링 * fix: article_main_card 테이블의 workbooks 컬럼의 디폴트 값({}) 대응 * chore: 미사용중인 주석 삭제 * feat: 기존 article_main_card 테이블에 존재하던 데이터도 지원되도록 반영 * refactor: ReadArticlesUseCase 리펙토링 * refactor: record class 명 수정(MemberIdAndNameRecord) --- .../repo/dao/article/ArticleMainCardDao.kt | 67 ++++++------------- .../ArticleMainCardExcludeWorkbookCommand.kt | 16 +++++ .../UpdateArticleMainCardWorkbookCommand.kt | 11 +++ .../article/support/ArticleMainCardMapper.kt | 41 +++++++----- .../com/few/api/repo/dao/member/MemberDao.kt | 10 +-- ...erIdRecord.kt => MemberIdAndNameRecord.kt} | 3 +- .../service/ArticleMainCardService.kt | 58 ++++++++++++++++ .../AppendWorkbookToArticleMainCardInDto.kt | 6 ++ .../dto/InitializeArticleMainCardInDto.kt | 16 +++++ .../document/usecase/AddArticleUseCase.kt | 31 +++++++-- .../document/usecase/MapArticleUseCase.kt | 7 ++ .../article/usecase/ReadArticlesUseCase.kt | 58 +++++++--------- .../usecase/dto/ReadArticleUseCaseOut.kt | 4 +- .../article/response/ReadArticleResponse.kt | 4 +- .../resources/messages/article.properties | 1 + .../com/few/data/common/code/CategoryType.kt | 4 ++ 16 files changed, 225 insertions(+), 112 deletions(-) create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/ArticleMainCardExcludeWorkbookCommand.kt create mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/UpdateArticleMainCardWorkbookCommand.kt rename api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/{MemberIdRecord.kt => MemberIdAndNameRecord.kt} (52%) create mode 100644 api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/AppendWorkbookToArticleMainCardInDto.kt create mode 100644 api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/InitializeArticleMainCardInDto.kt diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt index 3a06182eb..952aa1b8d 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt @@ -1,13 +1,11 @@ package com.few.api.repo.dao.article -import jooq.jooq_dsl.tables.ArticleMst.ARTICLE_MST -import jooq.jooq_dsl.tables.Member.MEMBER +import com.few.api.repo.dao.article.command.ArticleMainCardExcludeWorkbookCommand +import com.few.api.repo.dao.article.command.UpdateArticleMainCardWorkbookCommand import com.few.api.repo.dao.article.record.ArticleMainCardRecord import com.few.api.repo.dao.article.support.CommonJsonMapper import com.few.api.repo.dao.article.support.ArticleMainCardMapper import jooq.jooq_dsl.tables.ArticleMainCard.ARTICLE_MAIN_CARD -import jooq.jooq_dsl.tables.MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE -import jooq.jooq_dsl.tables.Workbook.WORKBOOK import org.jooq.* import org.jooq.impl.DSL.* import org.springframework.stereotype.Repository @@ -43,47 +41,14 @@ class ArticleMainCardDao( .where(ARTICLE_MAIN_CARD.ID.`in`(articleIds)) .query - fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbook(articleIds: Set): Set { - return selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds) - .fetch(articleMainCardMapper) - .toSet() - } - - private fun selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbookQuery(articleIds: Set) = - dslContext.select( - ARTICLE_MST.ID.`as`(ArticleMainCardRecord::articleId.name), - ARTICLE_MST.TITLE.`as`(ArticleMainCardRecord::articleTitle.name), - ARTICLE_MST.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name), - ARTICLE_MST.CATEGORY_CD.`as`(ArticleMainCardRecord::categoryCd.name), - ARTICLE_MST.CREATED_AT.`as`(ArticleMainCardRecord::createdAt.name), - MEMBER.ID.`as`(ArticleMainCardRecord::writerId.name), - MEMBER.EMAIL.`as`(ArticleMainCardRecord::writerEmail.name), - jsonGetAttributeAsText(MEMBER.DESCRIPTION, "name").`as`(ArticleMainCardRecord::writerName.name), - jsonGetAttribute(MEMBER.DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerImgUrl.name), - jsonArrayAgg( - jsonObject( - key("id").value(WORKBOOK.ID), - key("title").value(WORKBOOK.TITLE) - ) - ).`as`(ArticleMainCardRecord::workbooks.name) - ) - .from(ARTICLE_MST) - .join(MEMBER).on(ARTICLE_MST.MEMBER_ID.eq(MEMBER.ID)).and(ARTICLE_MST.DELETED_AT.isNull) - .and(MEMBER.DELETED_AT.isNull) - .leftJoin(MAPPING_WORKBOOK_ARTICLE).on(ARTICLE_MST.ID.eq(MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID)).and(MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) - .leftJoin(WORKBOOK).on(MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(WORKBOOK.ID)).and(WORKBOOK.DELETED_AT.isNull) - .where(ARTICLE_MST.ID.`in`(articleIds)) - .groupBy(ARTICLE_MST.ID) - .query + /** + * NOTE - The query performed in this function do not save the workbook. + */ + fun insertArticleMainCard(command: ArticleMainCardExcludeWorkbookCommand) = + insertArticleMainCardQuery(command).execute() - fun insertArticleMainCardsBulk(commands: Set) { - dslContext.batch( - commands.map { command -> insertArticleMainCardsBulkQuery(command) } - ).execute() - } - - fun insertArticleMainCardsBulkQuery(command: ArticleMainCardRecord) = - dslContext.insertInto( + fun insertArticleMainCardQuery(command: ArticleMainCardExcludeWorkbookCommand) = dslContext + .insertInto( ARTICLE_MAIN_CARD, ARTICLE_MAIN_CARD.ID, ARTICLE_MAIN_CARD.TITLE, @@ -92,8 +57,7 @@ class ArticleMainCardDao( ARTICLE_MAIN_CARD.CREATED_AT, ARTICLE_MAIN_CARD.WRITER_ID, ARTICLE_MAIN_CARD.WRITER_EMAIL, - ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, - ARTICLE_MAIN_CARD.WORKBOOKS + ARTICLE_MAIN_CARD.WRITER_DESCRIPTION ).values( command.articleId, command.articleTitle, @@ -109,7 +73,14 @@ class ArticleMainCardDao( "url" to command.writerImgUrl ) ) - ), - JSON.valueOf(articleMainCardMapper.toJsonStr(command.workbooks)) + ) ) + + fun updateArticleMainCardSetWorkbook(command: UpdateArticleMainCardWorkbookCommand) = + updateArticleMainCardSetWorkbookQuery(command).execute() + + fun updateArticleMainCardSetWorkbookQuery(command: UpdateArticleMainCardWorkbookCommand) = dslContext + .update(ARTICLE_MAIN_CARD) + .set(ARTICLE_MAIN_CARD.WORKBOOKS, JSON.valueOf(articleMainCardMapper.toJsonStr(command.workbooks))) + .where(ARTICLE_MAIN_CARD.ID.eq(command.articleId)) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/ArticleMainCardExcludeWorkbookCommand.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/ArticleMainCardExcludeWorkbookCommand.kt new file mode 100644 index 000000000..0392e27c1 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/ArticleMainCardExcludeWorkbookCommand.kt @@ -0,0 +1,16 @@ +package com.few.api.repo.dao.article.command + +import java.net.URL +import java.time.LocalDateTime + +data class ArticleMainCardExcludeWorkbookCommand( + val articleId: Long, + val articleTitle: String, + val mainImageUrl: URL, + val categoryCd: Byte, + val createdAt: LocalDateTime, + val writerId: Long, + val writerEmail: String, + val writerName: String, + val writerImgUrl: URL, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/UpdateArticleMainCardWorkbookCommand.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/UpdateArticleMainCardWorkbookCommand.kt new file mode 100644 index 000000000..d828ab363 --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/command/UpdateArticleMainCardWorkbookCommand.kt @@ -0,0 +1,11 @@ +package com.few.api.repo.dao.article.command + +data class UpdateArticleMainCardWorkbookCommand( + val articleId: Long, + val workbooks: List, +) + +data class WorkbookCommand( + val id: Long, + val title: String, +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/support/ArticleMainCardMapper.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/support/ArticleMainCardMapper.kt index 09b37ef92..7c4896b53 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/support/ArticleMainCardMapper.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/support/ArticleMainCardMapper.kt @@ -2,6 +2,7 @@ package com.few.api.repo.dao.article.support import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import com.few.api.repo.dao.article.command.WorkbookCommand import com.few.api.repo.dao.article.record.ArticleMainCardRecord import com.few.api.repo.dao.article.record.WorkbookRecord import org.jooq.JSON @@ -15,22 +16,28 @@ import java.time.LocalDateTime class ArticleMainCardMapper( private val objectMapper: ObjectMapper, ) : RecordMapper { - override fun map(record: Record): ArticleMainCardRecord { - val workbooksJsonArrayStr: String = record.get(ArticleMainCardRecord::workbooks.name, JSON::class.java).data() + override fun map(record: Record) = ArticleMainCardRecord( + articleId = record.get(ArticleMainCardRecord::articleId.name, Long::class.java), + articleTitle = record.get(ArticleMainCardRecord::articleTitle.name, String::class.java), + mainImageUrl = record.get(ArticleMainCardRecord::mainImageUrl.name, URL::class.java), + categoryCd = record.get(ArticleMainCardRecord::categoryCd.name, Byte::class.java), + createdAt = record.get(ArticleMainCardRecord::createdAt.name, LocalDateTime::class.java), + writerId = record.get(ArticleMainCardRecord::writerId.name, Long::class.java), + writerEmail = record.get(ArticleMainCardRecord::writerEmail.name, String::class.java), + writerName = record.get(ArticleMainCardRecord::writerName.name, String::class.java), + writerImgUrl = record.get(ArticleMainCardRecord::writerImgUrl.name, URL::class.java), + workbooks = record.get(ArticleMainCardRecord::workbooks.name, JSON::class.java)?.data()?.let { + if ("{}".equals(it)) { + emptyList() + } else { + val workbookRecords = objectMapper.readValue>(it) + workbookRecords.filter { w -> w.id != null && w.title != null } + .toList() + } + } ?: run { + emptyList() + } + ) - return ArticleMainCardRecord( - articleId = record.get(ArticleMainCardRecord::articleId.name, Long::class.java), - articleTitle = record.get(ArticleMainCardRecord::articleTitle.name, String::class.java), - mainImageUrl = record.get(ArticleMainCardRecord::mainImageUrl.name, URL::class.java), - categoryCd = record.get(ArticleMainCardRecord::categoryCd.name, Byte::class.java), - createdAt = record.get(ArticleMainCardRecord::createdAt.name, LocalDateTime::class.java), - writerId = record.get(ArticleMainCardRecord::writerId.name, Long::class.java), - writerEmail = record.get(ArticleMainCardRecord::writerEmail.name, String::class.java), - writerName = record.get(ArticleMainCardRecord::writerName.name, String::class.java), - writerImgUrl = record.get(ArticleMainCardRecord::writerImgUrl.name, URL::class.java), - workbooks = objectMapper.readValue>(workbooksJsonArrayStr) - ) - } - - fun toJsonStr(workbooks: List) = objectMapper.writeValueAsString(workbooks) + fun toJsonStr(workbooks: List) = objectMapper.writeValueAsString(workbooks) } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/MemberDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/MemberDao.kt index 14833a29f..f19a5f8b0 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/MemberDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/MemberDao.kt @@ -12,7 +12,7 @@ import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery import com.few.api.repo.dao.member.query.SelectWriterQuery import com.few.api.repo.dao.member.query.SelectWritersQuery import com.few.api.repo.dao.member.record.MemberIdAndIsDeletedRecord -import com.few.api.repo.dao.member.record.MemberIdRecord +import com.few.api.repo.dao.member.record.MemberIdAndNameRecord import com.few.api.repo.dao.member.record.MemberEmailAndTypeRecord import com.few.api.repo.dao.member.record.WriterRecord import com.few.api.repo.dao.member.record.WriterRecordMappedWorkbook @@ -22,6 +22,7 @@ import jooq.jooq_dsl.tables.MappingWorkbookArticle import jooq.jooq_dsl.tables.Member import org.jooq.DSLContext import org.jooq.impl.DSL +import org.jooq.impl.DSL.jsonGetAttributeAsText import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -124,13 +125,14 @@ class MemberDao( .where(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code)) .and(Member.MEMBER.DELETED_AT.isNull) - fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdRecord? { + fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdAndNameRecord? { return selectMemberByEmailQuery(query) - .fetchOneInto(MemberIdRecord::class.java) + .fetchOneInto(MemberIdAndNameRecord::class.java) } fun selectMemberByEmailQuery(query: SelectMemberByEmailQuery) = dslContext.select( - Member.MEMBER.ID.`as`(MemberIdRecord::memberId.name) + Member.MEMBER.ID.`as`(MemberIdAndNameRecord::memberId.name), + jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(MemberIdAndNameRecord::writerName.name) // writer only(nullable) ) .from(Member.MEMBER) .where(Member.MEMBER.EMAIL.eq(query.email)) diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdAndNameRecord.kt similarity index 52% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdAndNameRecord.kt index 73215fb6b..f2267e120 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdAndNameRecord.kt @@ -1,5 +1,6 @@ package com.few.api.repo.dao.member.record -data class MemberIdRecord( +data class MemberIdAndNameRecord( val memberId: Long, + val writerName: String?, ) \ No newline at end of file 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 new file mode 100644 index 000000000..9d7643e40 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt @@ -0,0 +1,58 @@ +package com.few.api.domain.admin.document.service + +import com.few.api.domain.admin.document.service.dto.AppendWorkbookToArticleMainCardInDto +import com.few.api.domain.admin.document.service.dto.InitializeArticleMainCardInDto +import com.few.api.exception.common.NotFoundException +import com.few.api.repo.dao.article.ArticleMainCardDao +import com.few.api.repo.dao.article.command.ArticleMainCardExcludeWorkbookCommand +import com.few.api.repo.dao.article.command.UpdateArticleMainCardWorkbookCommand +import com.few.api.repo.dao.article.command.WorkbookCommand +import com.few.api.repo.dao.article.record.ArticleMainCardRecord +import com.few.api.repo.dao.workbook.WorkbookDao +import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery +import org.springframework.stereotype.Service + +@Service +class ArticleMainCardService( + val articleMainCardDao: ArticleMainCardDao, + val workbookDao: WorkbookDao, +) { + fun initialize(inDto: InitializeArticleMainCardInDto) { + articleMainCardDao.insertArticleMainCard( + ArticleMainCardExcludeWorkbookCommand( + articleId = inDto.articleId, + articleTitle = inDto.articleTitle, + mainImageUrl = inDto.mainImageUrl, + categoryCd = inDto.categoryCd, + createdAt = inDto.createdAt, + writerId = inDto.writerId, + writerEmail = inDto.writerEmail, + writerName = inDto.writerName, + writerImgUrl = inDto.writerImgUrl + ) + ) + } + + fun appendWorkbook(inDto: AppendWorkbookToArticleMainCardInDto) { + val workbookRecord = workbookDao.selectWorkBook(SelectWorkBookRecordQuery(inDto.workbookId)) + ?: throw NotFoundException("workbook.notfound.id") + + val toBeAddedWorkbook = WorkbookCommand(inDto.workbookId, workbookRecord.title) + + val articleMainCardRecord: ArticleMainCardRecord = + articleMainCardDao.selectArticleMainCardsRecord(setOf(inDto.articleId)) + .ifEmpty { throw NotFoundException("articlemaincard.notfound.id") } + .first() + + val workbookCommands = + articleMainCardRecord.workbooks.map { WorkbookCommand(it.id!!, it.title!!) }.toMutableList() + workbookCommands.add(toBeAddedWorkbook) + + articleMainCardDao.updateArticleMainCardSetWorkbook( + UpdateArticleMainCardWorkbookCommand( + articleId = inDto.articleId, + workbooks = workbookCommands + ) + ) + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/AppendWorkbookToArticleMainCardInDto.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/AppendWorkbookToArticleMainCardInDto.kt new file mode 100644 index 000000000..6c5c2dcaa --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/AppendWorkbookToArticleMainCardInDto.kt @@ -0,0 +1,6 @@ +package com.few.api.domain.admin.document.service.dto + +data class AppendWorkbookToArticleMainCardInDto( + val articleId: Long, + val workbookId: Long, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/InitializeArticleMainCardInDto.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/InitializeArticleMainCardInDto.kt new file mode 100644 index 000000000..d013a7484 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/dto/InitializeArticleMainCardInDto.kt @@ -0,0 +1,16 @@ +package com.few.api.domain.admin.document.service.dto + +import java.net.URL +import java.time.LocalDateTime + +data class InitializeArticleMainCardInDto( + val articleId: Long, + val articleTitle: String, + val mainImageUrl: URL, + val categoryCd: Byte, + val createdAt: LocalDateTime, + val writerId: Long, + val writerEmail: String, + val writerName: String, + val writerImgUrl: URL, +) \ No newline at end of file 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 26ebbe42e..910be40b7 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 @@ -1,9 +1,11 @@ package com.few.api.domain.admin.document.usecase +import com.few.api.domain.admin.document.service.ArticleMainCardService import com.few.api.domain.admin.document.usecase.dto.AddArticleUseCaseIn import com.few.api.domain.admin.document.usecase.dto.AddArticleUseCaseOut import com.few.api.domain.admin.document.service.GetUrlService import com.few.api.domain.admin.document.service.dto.GetUrlInDto +import com.few.api.domain.admin.document.service.dto.InitializeArticleMainCardInDto import com.few.api.domain.admin.document.utils.ObjectPathGenerator import com.few.api.exception.common.ExternalIntegrationException import com.few.api.exception.common.NotFoundException @@ -25,6 +27,8 @@ import com.few.storage.document.service.PutDocumentService import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import java.io.File +import java.net.URL +import java.time.LocalDateTime import java.util.* @Component @@ -37,11 +41,12 @@ class AddArticleUseCase( private val convertDocumentService: ConvertDocumentService, private val putDocumentService: PutDocumentService, private val getUrlService: GetUrlService, + private val articleMainCardService: ArticleMainCardService, ) { @Transactional fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut { /** select writerId */ - val writerId = SelectMemberByEmailQuery(useCaseIn.writerEmail).let { + val writerIdRecord = SelectMemberByEmailQuery(useCaseIn.writerEmail).let { memberDao.selectMemberByEmail(it) } ?: throw NotFoundException("member.notfound.id") @@ -91,12 +96,15 @@ class AddArticleUseCase( } } + val category = CategoryType.fromName(useCaseIn.category) + ?: throw NotFoundException("article.invalid.category") + /** insert article */ val articleMstId = InsertFullArticleRecordCommand( - writerId = writerId.memberId, + writerId = writerIdRecord.memberId, mainImageURL = useCaseIn.articleImageUrl, title = useCaseIn.title, - category = CategoryType.convertToCode(useCaseIn.category), + category = category.code, content = htmlSource ).let { articleDao.insertFullArticleRecord(it) } @@ -123,10 +131,23 @@ class AddArticleUseCase( ArticleViewCountQuery( articleMstId, - CategoryType.fromCode(CategoryType.convertToCode(useCaseIn.category)) - ?: throw NotFoundException("article.invalid.category") + category ).let { articleViewCountDao.insertArticleViewCountToZero(it) } + articleMainCardService.initialize( + InitializeArticleMainCardInDto( + articleId = articleMstId, + articleTitle = useCaseIn.title, + mainImageUrl = useCaseIn.articleImageUrl, + categoryCd = category.code, + createdAt = LocalDateTime.now(), // TODO: DB insert 시점으로 변경 + writerId = writerIdRecord.memberId, + writerEmail = useCaseIn.writerEmail, + writerName = writerIdRecord.writerName ?: throw NotFoundException("article.writer.name"), + writerImgUrl = URL("https://github.com/user-attachments/assets/528a6531-2cba-4efc-b8df-64a083d38be8") //TODO: 작가 이미지로 변환 + ) + ) + return AddArticleUseCaseOut(articleMstId) } } \ No newline at end of file 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 1a7ed09ac..0cfefb7de 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 @@ -1,5 +1,7 @@ package com.few.api.domain.admin.document.usecase +import com.few.api.domain.admin.document.service.ArticleMainCardService +import com.few.api.domain.admin.document.service.dto.AppendWorkbookToArticleMainCardInDto import com.few.api.domain.admin.document.usecase.dto.MapArticleUseCaseIn import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.command.MapWorkBookToArticleCommand @@ -9,11 +11,16 @@ import org.springframework.transaction.annotation.Transactional @Component class MapArticleUseCase( private val workbookDao: WorkbookDao, + private val articleMainCardService: ArticleMainCardService, ) { @Transactional fun execute(useCaseIn: MapArticleUseCaseIn) { MapWorkBookToArticleCommand(useCaseIn.workbookId, useCaseIn.articleId, useCaseIn.dayCol).let { command -> workbookDao.mapWorkBookToArticle(command) } + + articleMainCardService.appendWorkbook( + AppendWorkbookToArticleMainCardInDto(useCaseIn.articleId, useCaseIn.workbookId) + ) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticlesUseCase.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticlesUseCase.kt index 401723eae..c6c750310 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticlesUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/ReadArticlesUseCase.kt @@ -23,9 +23,11 @@ class ReadArticlesUseCase( private val articleDao: ArticleDao, ) { - @Transactional // TODO: read only로 할 수 있도록 점검 + @Transactional(readOnly = true) fun execute(useCaseIn: ReadArticlesUseCaseIn): ReadArticlesUseCaseOut { - // 1. 아티클 조회수에서 마지막 읽은 아티클아이디, 카테고리를 기반으로 조회수 상위 10개를 가져옴 + /** + * 아티클 조회수 테이블에서 마지막 읽은 아티클 아이디, 카테고리를 기반으로 Offset(테이블 row 순위)을 구함 + */ val offset = if (useCaseIn.prevArticleId <= 0) { 0L } else { @@ -34,7 +36,10 @@ class ReadArticlesUseCase( ) ?: 0 } - // 이번 스크롤에서 보여줄 아티클 ID에 대한 기준 (Criterion) + /** + * 구한 Offset을 기준으로 이번 스크롤에서 보여줄 아티클 11개를 뽑아옴 + * 카테고리 별, 조회수 순 11개. 조회수가 같을 경우 최신 아티클이 우선순위를 가짐 + */ val articleViewsRecords: MutableList = articleViewCountDao.selectArticlesOrderByViews( SelectArticlesOrderByViewsQuery( offset, @@ -42,6 +47,9 @@ class ReadArticlesUseCase( ) ).toMutableList() + /** + * 11개를 조회한 상황에서 11개가 조회되지 않았다면 마지막 스크롤로 판단 + */ val isLast = if (articleViewsRecords.size == 11) { articleViewsRecords.removeAt(10) false @@ -49,38 +57,23 @@ class ReadArticlesUseCase( true } - // 2. TODO: 조회한 10개의 아티클 아이디를 기반으로 로컬 캐시에 있는지 조회(아티클 단건조회 캐시 사용) - // 3. TODO: 로컬캐시에 없으면 ARTICLE_MAIN_CARD 테이블에서 데이터가 있는지 조회 (컨텐츠는 article_ifo에서) - - // 4. ARTICLE_MAIN_CARD 테이블에도 없으면 조인 진행 후 ARTICLE_MAIN_CARD 테이블 및 캐시에 넣기 (컨텐츠는 article_ifo에서) - var existInArticleMainCardRecords: Set = + /** + * ARTICLE_MAIN_CARD 테이블에서 이번 스크롤에서 보여줄 10개 아티클 조회 (TODO: 캐싱 적용) + */ + var articleMainCardRecords: Set = articleMainCardDao.selectArticleMainCardsRecord(articleViewsRecords.map { it.articleId }.toSet()) - if (existInArticleMainCardRecords.size != articleViewsRecords.size) { - val existInArticleMainCardIds = existInArticleMainCardRecords.map { it.articleId }.toSet() - val notExistArticleMainCardTableArticleIds = articleViewsRecords - .filterNot { existInArticleMainCardIds.contains(it.articleId) } - .map { it.articleId } - .toSet() - - // join 진행하여 Select - val joinedArticleMainCardRecords: Set = articleMainCardDao - .selectByArticleMstAndMemberAndMappingWorkbookArticleAndWorkbook(notExistArticleMainCardTableArticleIds) - - // 결과를 MainCard 테이블에 저장 - articleMainCardDao.insertArticleMainCardsBulk(joinedArticleMainCardRecords) // TODO: 트랜잭션 분리 점검 - - existInArticleMainCardRecords = (existInArticleMainCardRecords + joinedArticleMainCardRecords).toSet() - - // TODO: 결과를 로컬 캐시에 저장 - } - - // 아티클 컨텐츠 조회 + /** + * 아티클 컨텐츠는 ARTICLE_MAIN_CARD가 아닌 ARTICLE_IFO에서 조회 (TODO: 캐싱 적용) + */ val selectArticleContentsRecords: List = - articleDao.selectArticleContents(existInArticleMainCardRecords.map { it.articleId }.toSet()) - setContentsToRecords(selectArticleContentsRecords, existInArticleMainCardRecords) + articleDao.selectArticleContents(articleMainCardRecords.map { it.articleId }.toSet()) + setContentsToRecords(selectArticleContentsRecords, articleMainCardRecords) - val sortedArticles = updateAndSortArticleViews(existInArticleMainCardRecords, articleViewsRecords) + /** + * 아티클 조회수 순, 조회수가 같을 경우 최신 아티클이 우선순위를 가지도록 정렬 (TODO: 삭제시 양향도 파악 필요) + */ + val sortedArticles = updateAndSortArticleViews(articleMainCardRecords, articleViewsRecords) val articleUseCaseOuts: List = sortedArticles.map { a -> ReadArticleUseCaseOut( @@ -99,8 +92,7 @@ class ReadArticlesUseCase( createdAt = a.createdAt, views = a.views, workbooks = a.workbooks - .filter { it.id != null && it.title != null } - .map { WorkbookDetail(it.id, it.title) } + .map { WorkbookDetail(it.id!!, it.title!!) } ) }.toList() diff --git a/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleUseCaseOut.kt b/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleUseCaseOut.kt index 81ade1469..a5ac939cd 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleUseCaseOut.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/usecase/dto/ReadArticleUseCaseOut.kt @@ -23,6 +23,6 @@ data class WriterDetail( ) data class WorkbookDetail( - val id: Long?, - val title: String?, + val id: Long, + val title: String, ) \ No newline at end of file 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 f07deaf2f..d587c3ce6 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 @@ -25,6 +25,6 @@ data class WriterInfo( ) data class WorkbookInfo( - val id: Long?, - val title: String?, + val id: Long, + val title: String, ) \ No newline at end of file diff --git a/api/src/main/resources/messages/article.properties b/api/src/main/resources/messages/article.properties index 71025d20e..e1f4acb6e 100644 --- a/api/src/main/resources/messages/article.properties +++ b/api/src/main/resources/messages/article.properties @@ -1,3 +1,4 @@ article.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0069\u0064 article.notfound.articleidworkbookid=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0069\u0064\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u0069 article.invalid.category=\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\ub294\u0020\uc544\ud2f0\ud074\u0020\uce74\ud14c\uace0\ub9ac\uc785\ub2c8\ub2e4\u000d +articlemaincard.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0020\u006d\u0061\u0069\u006e\u0020\u0063\u0061\u0072\u0064\uac00\u0020\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\uc2b5\ub2c8\ub2e4\u000d 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 94aa4ed96..60838cda3 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 @@ -17,6 +17,10 @@ enum class CategoryType(val code: Byte, val displayName: String) { return entries.find { it.code == code } } + fun fromName(displayName: String): CategoryType? { + return entries.find { it.displayName.equals(displayName) } + } + fun convertToCode(displayName: String): Byte { return entries.find { it.name == displayName }?.code ?: throw IllegalArgumentException("Invalid category name $displayName") }