diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt index 980cf3da6..326efda44 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt @@ -56,14 +56,17 @@ class ProblemDao( .let { ProblemIdsRecord(it) } } - fun insertProblems(command: InsertProblemsCommand) { - dslContext.insertInto(Problem.PROBLEM) - .set(Problem.PROBLEM.ARTICLE_ID, command.articleId) - .set(Problem.PROBLEM.CREATOR_ID, command.createrId) - .set(Problem.PROBLEM.TITLE, command.title) - .set(Problem.PROBLEM.CONTENTS, JSON.valueOf(contentsJsonMapper.toJson(command.contents))) - .set(Problem.PROBLEM.ANSWER, command.answer) - .set(Problem.PROBLEM.EXPLANATION, command.explanation) - .execute() + fun insertProblems(command: List) { + dslContext.batch( + command.map { + dslContext.insertInto(Problem.PROBLEM) + .set(Problem.PROBLEM.ARTICLE_ID, it.articleId) + .set(Problem.PROBLEM.CREATOR_ID, it.createrId) + .set(Problem.PROBLEM.TITLE, it.title) + .set(Problem.PROBLEM.CONTENTS, JSON.valueOf(contentsJsonMapper.toJson(it.contents))) + .set(Problem.PROBLEM.ANSWER, it.answer) + .set(Problem.PROBLEM.EXPLANATION, it.explanation) + } + ).execute() } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/dto/AddArticleUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/dto/AddArticleUseCaseIn.kt index cb1163af5..d11df1b37 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/dto/AddArticleUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/dto/AddArticleUseCaseIn.kt @@ -9,8 +9,9 @@ data class AddArticleUseCaseIn( val title: String, val category: String, /** Article IFO */ + val contentType: String, val contentSource: String, - val problemData: ProblemDetail + val problems: List ) 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 2517e0a61..5fc2d3194 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 @@ -2,8 +2,13 @@ package com.few.api.domain.admin.document.usecase import com.few.api.domain.admin.document.dto.AddArticleUseCaseIn import com.few.api.domain.admin.document.dto.AddArticleUseCaseOut +import com.few.api.domain.admin.document.service.GetUrlService +import com.few.api.domain.admin.document.service.dto.GetUrlQuery +import com.few.api.domain.admin.document.utils.ObjectPathGenerator import com.few.api.repo.dao.article.ArticleDao import com.few.api.repo.dao.article.command.InsertFullArticleRecordCommand +import com.few.api.repo.dao.document.DocumentDao +import com.few.api.repo.dao.document.command.InsertDocumentIfoCommand import com.few.api.repo.dao.member.MemberDao import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery import com.few.api.repo.dao.problem.ProblemDao @@ -11,14 +16,22 @@ import com.few.api.repo.dao.problem.command.InsertProblemsCommand import com.few.api.repo.dao.problem.support.Content import com.few.api.repo.dao.problem.support.Contents import com.few.data.common.code.CategoryType +import com.few.storage.document.service.ConvertDocumentService +import com.few.storage.document.service.PutDocumentService import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import java.io.File +import java.util.* @Component class AddArticleUseCase( private val articleDao: ArticleDao, private val memberDao: MemberDao, - private val problemDao: ProblemDao + private val problemDao: ProblemDao, + private val documentDao: DocumentDao, + private val convertDocumentService: ConvertDocumentService, + private val putDocumentService: PutDocumentService, + private val getUrlService: GetUrlService ) { @Transactional fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut { @@ -27,32 +40,78 @@ class AddArticleUseCase( memberDao.selectMemberByEmail(it) } ?: throw RuntimeException("writer not found") + /** + * - 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 mdSource = useCaseIn.contentSource + val htmlSource = convertDocumentService.mdToHtml(useCaseIn.contentSource) + + val document = runCatching { + File.createTempFile("temp", ".md") + }.onSuccess { + it.writeText(mdSource) + }.getOrThrow() + val documentName = ObjectPathGenerator.documentPath("md") + + putDocumentService.execute(documentName, document)?.let { res -> + val source = res.`object` + GetUrlQuery(source).let { query -> + getUrlService.execute(query) + }.let { url -> + InsertDocumentIfoCommand( + path = documentName, + url = url + ).let { command -> + documentDao.insertDocumentIfo(command) + } + url + } + } ?: throw IllegalStateException("Failed to put document") + + htmlSource + } + useCaseIn.contentType.lowercase(Locale.getDefault()) == "html" -> { + useCaseIn.contentSource + } + else -> { + throw IllegalArgumentException("content type is not supported") + } + } + /** insert article */ val articleMstId = InsertFullArticleRecordCommand( writerId = writerId.memberId, mainImageURL = useCaseIn.articleImageUrl, title = useCaseIn.title, category = CategoryType.convertToCode(useCaseIn.category), - content = useCaseIn.contentSource + content = htmlSource ).let { articleDao.insertFullArticleRecord(it) } /** insert problems */ - InsertProblemsCommand( - articleId = articleMstId, - createrId = 0L, // todo fix - title = useCaseIn.title, - contents = Contents( - useCaseIn.problemData.contents.map { - Content( - it.number, - it.content - ) - } - ), - answer = useCaseIn.problemData.answer, - explanation = useCaseIn.problemData.explanation - ).let { - problemDao.insertProblems(it) + useCaseIn.problems.stream().map { problemDatum -> + InsertProblemsCommand( + articleId = articleMstId, + createrId = 0L, // todo fix + title = problemDatum.title, + contents = Contents( + problemDatum.contents.map { detail -> + Content( + detail.number, + detail.content + ) + } + ), + answer = problemDatum.answer, + explanation = problemDatum.explanation + ) + }.toList().let { commands -> + problemDao.insertProblems(commands) } return AddArticleUseCaseOut(articleMstId) 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 cb308f31c..b084fb1ce 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 @@ -8,7 +8,7 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration -@EnableWebMvc +@EnableWebMvc // todo refactor class WebConfig : WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") diff --git a/api/src/main/kotlin/com/few/api/web/controller/admin/AdminController.kt b/api/src/main/kotlin/com/few/api/web/controller/admin/AdminController.kt index 316270786..f7b1ea0a2 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/admin/AdminController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/admin/AdminController.kt @@ -57,18 +57,21 @@ class AdminController( articleImageUrl = request.articleImageUrl, title = request.title, category = request.category, + contentType = request.contentType, contentSource = request.contentSource, - problemData = ProblemDetail( - title = request.problemData.title, - contents = request.problemData.contents.map { - ProblemContentDetail( - number = it.number, - content = it.content - ) - }, - answer = request.problemData.answer, - explanation = request.problemData.explanation - ) + problems = request.problemData.map { datum -> + ProblemDetail( + title = datum.title, + contents = datum.contents.map { detail -> + ProblemContentDetail( + number = detail.number, + content = detail.content + ) + }, + answer = datum.answer, + explanation = datum.explanation + ) + }.toList() ).let { useCaseIn -> addArticleUseCase.execute(useCaseIn) } diff --git a/api/src/main/kotlin/com/few/api/web/controller/admin/request/AddArticleRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/admin/request/AddArticleRequest.kt index 483fc12fb..eada423dd 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/admin/request/AddArticleRequest.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/admin/request/AddArticleRequest.kt @@ -14,9 +14,11 @@ data class AddArticleRequest( @field:NotBlank(message = "{category.notblank}") val category: String, /** Article IFO */ + @NotBlank(message = "{content.type.notblank}") + val contentType: String, @field:NotBlank(message = "{content.source.notblank}") val contentSource: String, - val problemData: ProblemDto + val problemData: List ) data class ProblemDto( diff --git a/api/src/main/resources/ValidationMessages.properties b/api/src/main/resources/ValidationMessages.properties index f15dea708..f7353f561 100644 --- a/api/src/main/resources/ValidationMessages.properties +++ b/api/src/main/resources/ValidationMessages.properties @@ -5,6 +5,7 @@ image.url.notblank=Image URL must be blank title.notblank=Title must not be blank category.notblank=Category must not be blank content.source.notblank=Content must not be blank +content.type.notblank=Content Type must not be blank problem.title.notblank=Problem Title must not be blank problem.content.notblank=Problem Content must not be blank problem.answer.notblank=Problem Answer must not be blank @@ -14,4 +15,3 @@ workbook.description.notblank=Workbook Description must not be blank min.day=The Day field must be greater than or equal to 1 min.id=The ID field must be greater than or equal to 1 min.problem.number=The Problem Number field must be greater than or equal to 1 -max.day=The Day field must be less than or equal to 7 diff --git a/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt index c4fbf3868..b563f37f6 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt @@ -137,17 +137,31 @@ class AdminControllerTest : ControllerTestSpec() { URL("http://localhost:8080"), "title", CategoryType.fromCode(0)!!.name, - "contentSource", - ProblemDto( - "title", - listOf( - ProblemContentDto(1L, "content1"), - ProblemContentDto(2L, "content2"), - ProblemContentDto(3L, "content3"), - ProblemContentDto(4L, "content4") + "md", + "content source", + listOf( + ProblemDto( + "title1", + listOf( + ProblemContentDto(1L, "content1"), + ProblemContentDto(2L, "content2"), + ProblemContentDto(3L, "content3"), + ProblemContentDto(4L, "content4") + ), + "1", + "explanation" ), - "1", - "explanation" + ProblemDto( + "title2", + listOf( + ProblemContentDto(1L, "content1"), + ProblemContentDto(2L, "content2"), + ProblemContentDto(3L, "content3"), + ProblemContentDto(4L, "content4") + ), + "2", + "explanation" + ) ) ) val body = objectMapper.writeValueAsString(request) @@ -159,17 +173,31 @@ class AdminControllerTest : ControllerTestSpec() { URL("http://localhost:8080"), "title", CategoryType.fromCode(0)!!.name, - "contentSource", - ProblemDetail( - "title", - listOf( - ProblemContentDetail(1L, "content1"), - ProblemContentDetail(2L, "content2"), - ProblemContentDetail(3L, "content3"), - ProblemContentDetail(4L, "content4") + "md", + "content source", + listOf( + ProblemDetail( + "title1", + listOf( + ProblemContentDetail(1L, "content1"), + ProblemContentDetail(2L, "content2"), + ProblemContentDetail(3L, "content3"), + ProblemContentDetail(4L, "content4") + ), + "1", + "explanation" ), - "1", - "explanation" + ProblemDetail( + "title2", + listOf( + ProblemContentDetail(1L, "content1"), + ProblemContentDetail(2L, "content2"), + ProblemContentDetail(3L, "content3"), + ProblemContentDetail(4L, "content4") + ), + "2", + "explanation" + ) ) ) ) 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 3e2286ef1..79e7358d5 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 @@ -4,17 +4,11 @@ package com.few.data.common.code * @see com.few.batch.data.common.code.BatchCategoryType */ enum class CategoryType(val code: Byte, val displayName: String) { - POLITICS(0, "정치"), - ECONOMY(10, "경제"), - SOCIETY(20, "사회"), - CULTURE(30, "문화"), - LIFE(40, "생활"), - IT(50, "IT"), - SCIENCE(60, "과학"), - ENTERTAINMENTS(70, "엔터테인먼트"), - SPORTS(80, "스포츠"), - GLOBAL(90, "국제"), - ETC(100, "기타"); + ECONOMY(0, "경제"), + IT(10, "IT"), + MARKETING(20, "마케팅"), + CULTURE(30, "교양"), + SCIENCE(40, "과학"); companion object { fun fromCode(code: Byte): CategoryType? { diff --git a/email/src/main/resources/templates/article.html b/email/src/main/resources/templates/article.html index 5e1404a96..b77bb6f72 100644 --- a/email/src/main/resources/templates/article.html +++ b/email/src/main/resources/templates/article.html @@ -1,13 +1,15 @@ - - + FEW - - - + " +> +
@@ -124,7 +181,10 @@
-
- few_logo + + few_logo
-

-

+
웹으로 읽을래요! + " + >웹으로 읽을래요!
@@ -51,10 +71,16 @@
- +
- + " + id="article-content" + th:utext="${articleContent}" + >
- - + + " + id="current-day" + th:text="${articleDay}" + >
-

+ " + id="article-title" + th:text="${articleTitle}" + >
- 작가 - 작가 + - - next + " + id="writer-name" + th:text="${writerName}" + > + + next
- 문제 풀러 가기 + " + >문제 풀러 가기
@@ -149,31 +211,40 @@ - instagram + instagram - link + link -

- 수신거부 + " + > + 수신거부

- diff --git a/resources/docker/scripts/api-start b/resources/docker/scripts/api-start index f7e675d91..d4981c561 100644 --- a/resources/docker/scripts/api-start +++ b/resources/docker/scripts/api-start @@ -1,6 +1,7 @@ #!/bin/sh cd .. +docker-compose -f docker-compose-api.yml pull docker-compose -f docker-compose-api.yml down docker-compose -f docker-compose-api.yml up -d -sleep 10 \ No newline at end of file +sleep 10 diff --git a/storage/src/main/kotlin/com/few/storage/document/service/ConvertDocumentService.kt b/storage/src/main/kotlin/com/few/storage/document/service/ConvertDocumentService.kt index 3eb7f01d8..f05d2c9e3 100644 --- a/storage/src/main/kotlin/com/few/storage/document/service/ConvertDocumentService.kt +++ b/storage/src/main/kotlin/com/few/storage/document/service/ConvertDocumentService.kt @@ -11,14 +11,31 @@ class ConvertDocumentService { companion object { val parser = Parser.builder().build() val htmlRenderer = HtmlRenderer.builder().build() - val HTML_HEADER = - " " + val ARTICLE = "
" } fun mdToHtml(md: String): String { - val html = Jsoup.parse(HTML_HEADER) - val body = htmlRenderer.render(parser.parse(md)) - html.body().append(body) - return html.toString() + val html = Jsoup.parse(ARTICLE) + val article = htmlRenderer.render(parser.parse(md)) + html.getElementsByTag("article").append(article) + html.getElementsByTag("h1").forEach { + it.attr("style", "font-size: 20px; line-height: 140%; font-weight: 600") + } + html.getElementsByTag("h2").forEach { + it.attr("style", "font-size: 20px; line-height: 140%; font-weight: 600") + } + html.getElementsByTag("h3").forEach { + it.attr("style", "font-size: 20px; line-height: 140%; font-weight: 600") + } + html.getElementsByTag("img").forEach { + it.attr("style", "max-height: 280px; object-fit: contain; max-width: 480px; margin-left: auto; margin-right: auto; width: 100%;") + } + html.getElementsByTag("article").forEach { + it.attr("style", "max-width: 480px; font-size: 15px; line-height: 170%; font-weight: 400;") + } + html.getElementsByTag("a").forEach { + it.attr("style", "overflow: hidden; word-break: break-all;") + } + return html.body().html() } } \ No newline at end of file