From f9920530fe19229853f5f2748eb5a69f9017f815 Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Fri, 28 Jun 2024 01:58:57 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Refactor/#117]=20=EB=B0=B0=EC=B9=98=20Writ?= =?UTF-8?q?er=20=EB=A6=AC=ED=8E=99=ED=86=A0=EB=A7=81=20(#118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: progress와 day가 달라 생기는 문제 해결 * feat: 마지막 아티클을 전송후 구독 해지하는 로직 추가 * !chore: 임시 article.html 추가 * feat: 메일 전송 성공한 멤버만 필터링 * refactor: 1b8402419dd2faa39699057b7344edfb1cde5531 미반영 테스트 수정 --- .../writer/WorkBookSubscriberWriter.kt | 37 ++++- .../WorkBookSubscriberWriterTestSetHelper.kt | 2 +- .../src/main/resources/templates/article.html | 126 ++++++++++++++++++ 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 email/src/main/resources/templates/article.html diff --git a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt index 540924bbd..fd5836213 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt @@ -11,9 +11,11 @@ import jooq.jooq_dsl.tables.MappingWorkbookArticle import jooq.jooq_dsl.tables.Member import jooq.jooq_dsl.tables.Subscription import org.jooq.DSLContext +import org.jooq.impl.DSL import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import java.time.LocalDate +import java.time.LocalDateTime data class MemberReceiveArticle( val workbookId: Long, @@ -70,6 +72,7 @@ class WorkBookSubscriberWriter( /** 구독자들이 구독한 학습지 ID와 구독자의 학습지 구독 진행률을 기준으로 구독자가 받을 학습지 정보를 조회한다.*/ val memberReceiveArticles = targetWorkBookProgress.keys.stream().map { workbookId -> + val dayCols = targetWorkBookProgress[workbookId]!!.stream().map { it + 1L }.toList() // todo check refactoring dslContext.select( mappingWorkbookArticleT.WORKBOOK_ID.`as`(MemberReceiveArticle::workbookId.name), @@ -80,7 +83,7 @@ class WorkBookSubscriberWriter( .where( mappingWorkbookArticleT.WORKBOOK_ID.eq(workbookId) ) - .and(mappingWorkbookArticleT.DAY_COL.`in`(targetWorkBookProgress[workbookId]!!)) + .and(mappingWorkbookArticleT.DAY_COL.`in`(dayCols)) .and(mappingWorkbookArticleT.DELETED_AT.isNull) .fetchInto(MemberReceiveArticle::class.java) }.flatMap { it.stream() }.toList().let { @@ -103,7 +106,7 @@ class WorkBookSubscriberWriter( // todo check !! target is not null val emailServiceArgs = items.map { val toEmail = memberEmailRecords[it.memberId]!! - val memberArticle = memberReceiveArticles.getByWorkBookIdAndDayCol(it.targetWorkBookId, it.progress) + val memberArticle = memberReceiveArticles.getByWorkBookIdAndDayCol(it.targetWorkBookId, it.progress + 1) val articleContent = articleContentsMap[memberArticle.articleId]!! return@map it.memberId to SendArticleEmailArgs(toEmail, ARTICLE_SUBJECT, ARTICLE_TEMPLATE, articleContent, ARTICLE_STYLE) } @@ -120,6 +123,29 @@ class WorkBookSubscriberWriter( } } + /** 워크북 마지막 학습지 DAY_COL을 조회한다 */ + val lastDayCol = dslContext.select( + mappingWorkbookArticleT.WORKBOOK_ID, + DSL.max(mappingWorkbookArticleT.DAY_COL) + ) + .from(mappingWorkbookArticleT) + .where(mappingWorkbookArticleT.WORKBOOK_ID.`in`(targetWorkBookIds)) + .and(mappingWorkbookArticleT.DELETED_AT.isNull) + .groupBy(mappingWorkbookArticleT.WORKBOOK_ID) + .fetch() + .intoMap(mappingWorkbookArticleT.WORKBOOK_ID, DSL.max(mappingWorkbookArticleT.DAY_COL)) + + /** 마지막 학습지를 받은 구독자들의 ID를 필터링한다.*/ + val receiveLastDayMembers = items.filter { + it.targetWorkBookId in lastDayCol.keys + }.filter { + (it.progress.toInt() + 1) == lastDayCol[it.targetWorkBookId] + }.map { + it.memberId + }.filter { + memberSuccessRecords[it] == true + } + val successMemberIds = memberSuccessRecords.filter { it.value }.keys /** 이메일 전송에 성공한 구독자들의 진행률을 업데이트한다.*/ dslContext.update(subscriptionT) @@ -128,6 +154,13 @@ class WorkBookSubscriberWriter( .and(subscriptionT.TARGET_WORKBOOK_ID.`in`(targetWorkBookIds)) .execute() + /** 마지막 학습지를 받은 구독자들은 구독을 해지한다.*/ + dslContext.update(subscriptionT) + .set(subscriptionT.DELETED_AT, LocalDateTime.now()) + .where(subscriptionT.MEMBER_ID.`in`(receiveLastDayMembers)) + .and(subscriptionT.TARGET_WORKBOOK_ID.`in`(targetWorkBookIds)) + .execute() + return if (failRecords.isNotEmpty()) { mapOf("success" to memberSuccessRecords, "fail" to failRecords) } else { diff --git a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt b/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt index 5db4d1d9f..cf9c48d8a 100644 --- a/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt +++ b/batch/src/test/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriterTestSetHelper.kt @@ -88,7 +88,7 @@ class WorkBookSubscriberWriterTestSetHelper( .where( MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID.eq(workbookId) ) - .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.eq(it[Subscription.SUBSCRIPTION.PROGRESS].toInt())) + .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL.eq(it[Subscription.SUBSCRIPTION.PROGRESS].toInt() + 1)) .and(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DELETED_AT.isNull) .fetchOneInto(String::class.java) diff --git a/email/src/main/resources/templates/article.html b/email/src/main/resources/templates/article.html new file mode 100644 index 000000000..7d8c63623 --- /dev/null +++ b/email/src/main/resources/templates/article.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + From 854f3ef2fd7bc829633138b57ff72fbd3b63816c Mon Sep 17 00:00:00 2001 From: belljun3395 <195850@jnu.ac.kr> Date: Mon, 1 Jul 2024 13:11:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Fix/#115=20:=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C=20=EB=B0=9C=EA=B2=AC?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용하지 않는 Mapper 삭제 * fix: ContentsJsonMapper의 키값 id -> number 수정 * refactor: @EnableWebMvc 제거 * refactor: contentsJsonMapper로 변환 구현하도록 수정 * refactor: SPRINGDOC 버전 1.6.3 -> 2.0.2 * feat: api 모듈에 jooq 의존성 추가 * refactor: openapi 의존성 webflux 버전으로 수정 * refactor: openapi3, postman의 url을 변수를 통해 받을 수 있도록 수정 * refactor: buildDockerImage 태스크 수행시 필요 변수 추가 * fix: 스키마 명 대문자로 수정 * fix: web 의존성 webflux -> web으로 변경 * fix: Swagger 생성을 보장할 수 있도록 수정 * feat: CORS 관련 설정 추가 * refactor: Jooq 관련 공통 의존성 최상의 gradle로 통일 * feat: jooqCodegenAll 테스크 추가 * refactor: 단일 조회시 예외 발생이 아니라 XXX? 값을 반환 하도록 수정 * fix: 변경된 jooqCodegen 반영 * fix: 76c7e3a159a81bd673e276b679c880762c74a696 미반영 테스트 수정 * feat: 임시 batch-cron 구현 * refactor: build시 test가 수행되어 순서 변경 및 중복 삭제 * feat: root debug 로깅 레벨 설정 * refactor: 매 오전 8시에 batch-cron 실행 되도록 수정 * chore: 주석 추가 및 테스크 이름 수정 * feat: WebConfig에 @EnableWebMvc 추가 및 resourceHandlers 추가 * refactor: SPRINGDOC 버전 수정 2.0.2 -> 1.6.3 * chore: EMAIL_PASSWORD 통일 및 local에 기본값 추가 * refactor: openapi-ui 의존성 수정 webflux -> openapi * fix: EMAIL_PASSWORD 통일 액션에 반영 * fix: openapi 의존성 오타 수정 * fix: generateSwaggerUIApi가 수행되고 generateStaticSwaggerUIApi가 수행되도록 수정 * refactor: 도커 이미지 빌드 과정 수정 * refactor: 시간 기록 추가 및 성공 여부 status 수정 --- .github/workflows/batch-cron.yml | 18 +++ .github/workflows/code-ci.yml | 20 ++- .github/workflows/integration-test.yml | 8 +- api-repo/build.gradle.kts | 118 ----------------- .../few/api/repo/dao/article/ArticleDao.kt | 6 +- .../com/few/api/repo/dao/member/MemberDao.kt | 9 +- .../{MemberRecord.kt => MemberIdRecord.kt} | 2 +- .../member/support/WriterDescriptionMapper.kt | 22 ---- .../few/api/repo/dao/problem/ProblemDao.kt | 8 +- .../dao/problem/support/ContentsJsonMapper.kt | 2 +- .../dao/problem/support/ContentsMapper.kt | 23 ---- .../few/api/repo/dao/workbook/WorkbookDao.kt | 3 +- .../api/repo/dao/article/ArticleDaoTest.kt | 4 +- .../support/WriterDescriptionMapperTest.kt | 60 --------- .../api/repo/dao/problem/ProblemDaoTest.kt | 2 +- .../problem/support/ContentsJsonMapperTest.kt | 4 +- .../dao/problem/support/ContentsMapperTest.kt | 72 ---------- .../api/repo/dao/workbook/WorkbookDaoTest.kt | 2 +- api/build.gradle.kts | 50 +++++-- .../kotlin/com/few/api/config/ApiConfig.kt | 2 - .../service/BrowseArticleProblemsService.kt | 2 +- .../article/usecase/ReadArticleUseCase.kt | 2 +- .../problem/usecase/CheckProblemUseCase.kt | 4 +- .../problem/usecase/ReadProblemUseCase.kt | 13 +- .../subscription/service/MemberService.kt | 5 +- .../usecase/SubscribeWorkbookUseCase.kt | 5 +- .../usecase/UnsubscribeAllUseCase.kt | 2 +- .../usecase/UnsubscribeWorkbookUseCase.kt | 2 +- .../usecase/ReadWorkBookArticleUseCase.kt | 2 +- .../workbook/usecase/ReadWorkbookUseCase.kt | 2 +- .../com/few/api/web/config/WebConfig.kt | 26 ++++ api/src/main/resources/application.yml | 3 + batch/build.gradle.kts | 111 ---------------- .../article/BatchSendArticleEmailService.kt | 16 ++- build.gradle.kts | 124 ++++++++++++++++++ .../entity/V1.00.0.0__draft_table_design.sql | 22 ++-- .../entity/V1.00.0.1__add_column.sql | 2 +- .../V1.00.0.2__add_subscription_progress.sql | 2 +- .../V1.00.0.3__batch_call_execution_table.sql | 2 +- .../resources/application-email-local.yml | 2 +- email/src/test/resources/application-test.yml | 2 +- 41 files changed, 291 insertions(+), 495 deletions(-) create mode 100644 .github/workflows/batch-cron.yml rename api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/{MemberRecord.kt => MemberIdRecord.kt} (71%) delete mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapper.kt delete mode 100644 api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsMapper.kt delete mode 100644 api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapperTest.kt delete mode 100644 api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsMapperTest.kt create mode 100644 api/src/main/kotlin/com/few/api/web/config/WebConfig.kt diff --git a/.github/workflows/batch-cron.yml b/.github/workflows/batch-cron.yml new file mode 100644 index 000000000..78a5aceac --- /dev/null +++ b/.github/workflows/batch-cron.yml @@ -0,0 +1,18 @@ +name: Batch Cron + +on: + schedule: + # 매 오전 8시에 실행 + - cron: '0 8 * * *' + workflow_dispatch: + +jobs: + code-review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + run: | + curl -X 'POST' \ + 'https://api.fewletter.site/batch/article' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' diff --git a/.github/workflows/code-ci.yml b/.github/workflows/code-ci.yml index 14e09dc2d..e9e382e73 100644 --- a/.github/workflows/code-ci.yml +++ b/.github/workflows/code-ci.yml @@ -10,7 +10,7 @@ permissions: env: RELEASE_VERSION: ${{ github.sha }} - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} jobs: build: @@ -25,19 +25,27 @@ jobs: - name: Jooq Code Generation run: | - ./gradlew --info jooqCodegen + ./gradlew --info jooqCodegenAll + + - name: Test with Gradle + run: | + ./gradlew --info test - name: Build with Gradle run: | - ./gradlew --info api:build + ./gradlew --info api:build -x test - - name: Test with Gradle + - name: Generate OpenAPI3 run: | - ./gradlew --info test + ./gradlew --info api:openapi3 -PserverUrl=https://api.fewletter.site + + - name: Generate Swagger + run: | + ./gradlew --info api:generateStaticSwaggerUIApi - name : Docker Login run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} - name: Build Docker Image run: | - ./gradlew --info api:buildDockerImage + ./gradlew --info api:buildDockerImage -PimageName=fewletter/api -PreleaseVersion=${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index feb063486..ac1462b88 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -9,7 +9,7 @@ permissions: contents: read env: - MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} + EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }} jobs: build: @@ -24,11 +24,7 @@ jobs: - name: Jooq Code Generation run: | - ./gradlew --info jooqCodegen - - - name: Build with Gradle - run: | - ./gradlew --info api:build + ./gradlew --info jooqCodegenAll - name: Test with Gradle run: | diff --git a/api-repo/build.gradle.kts b/api-repo/build.gradle.kts index 1a49821d5..a29bcaf5c 100644 --- a/api-repo/build.gradle.kts +++ b/api-repo/build.gradle.kts @@ -6,21 +6,6 @@ tasks.getByName("jar") { enabled = true } -plugins { - /** jooq */ - id("org.jooq.jooq-codegen-gradle") version DependencyVersion.JOOQ -} - -sourceSets { - main { - java { - val mainDir = "src/main/kotlin" - val jooqDir = "src/generated" - srcDirs(mainDir, jooqDir) - } - } -} - dependencies { /** module */ api(project(":data")) @@ -33,110 +18,7 @@ dependencies { implementation("org.flywaydb:flyway-core:${DependencyVersion.FLYWAY}") implementation("org.flywaydb:flyway-mysql") - /** jooq */ - implementation("org.springframework.boot:spring-boot-starter-jooq") - implementation("org.jooq:jooq:${DependencyVersion.JOOQ}") - implementation("org.jooq:jooq-meta:${DependencyVersion.JOOQ}") - implementation("org.jooq:jooq-codegen:${DependencyVersion.JOOQ}") - jooqCodegen("org.jooq:jooq-meta-extensions:${DependencyVersion.JOOQ}") - /** test container */ implementation(platform("org.testcontainers:testcontainers-bom:${DependencyVersion.TEST_CONTAINER}")) testImplementation("org.testcontainers:mysql") -} - -/** copy data migration */ -tasks.create("copyDataMigration") { - doLast { - val root = rootDir - val flyWayResourceDir = "/db/migration/entity" - val dataMigrationDir = "$root/data/$flyWayResourceDir" - File(dataMigrationDir).walkTopDown().forEach { - if (it.isFile) { - it.copyTo( - File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), - true - ) - } - } - } -} - -/** copy data migration before compile kotlin */ -tasks.getByName("compileKotlin") { - dependsOn("copyDataMigration") -} - -/** jooq codegen after copy data migration */ -tasks.getByName("jooqCodegen") { - dependsOn("copyDataMigration") -} - -jooq { - configuration { - generator { - database { - name = "org.jooq.meta.extensions.ddl.DDLDatabase" - properties { - // Specify the location of your SQL script. - // You may use ant-style file matching, e.g. /path/**/to/*.sql - // - // Where: - // - ** matches any directory subtree - // - * matches any number of characters in a directory / file name - // - ? matches a single character in a directory / file name - property { - key = "scripts" - value = "src/main/resources/db/migration/**/*.sql" - } - - // The sort order of the scripts within a directory, where: - // - // - semantic: sorts versions, e.g. v-3.10.0 is after v-3.9.0 (default) - // - alphanumeric: sorts strings, e.g. v-3.10.0 is before v-3.9.0 - // - flyway: sorts files the same way as flyway does - // - none: doesn't sort directory contents after fetching them from the directory - property { - key = "sort" - value = "flyway" - } - - // The default schema for unqualified objects: - // - // - public: all unqualified objects are located in the PUBLIC (upper case) schema - // - none: all unqualified objects are located in the default schema (default) - // - // This configuration can be overridden with the schema mapping feature - property { - key = "unqualifiedSchema" - value = "none" - } - - // The default name case for unquoted objects: - // - // - as_is: unquoted object names are kept unquoted - // - upper: unquoted object names are turned into upper case (most databases) - // - lower: unquoted object names are turned into lower case (e.g. PostgreSQL) - property { - key = "defaultNameCase" - value = "as_is" - } - } - } - - generate { - isDeprecated = false - isRecords = true - isImmutablePojos = true - isFluentSetters = true - isJavaTimeTypes = true - } - - target { - packageName = "jooq.jooq_dsl" - directory = "src/generated" - encoding = "UTF-8" - } - } - } } \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt index 8852a8049..1c103cd78 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleDao.kt @@ -16,7 +16,7 @@ import org.springframework.stereotype.Repository class ArticleDao( private val dslContext: DSLContext ) { - fun selectArticleRecord(query: SelectArticleRecordQuery): SelectArticleRecord { + fun selectArticleRecord(query: SelectArticleRecordQuery): SelectArticleRecord? { val articleId = query.articleId return dslContext.select( @@ -33,10 +33,9 @@ class ArticleDao( .where(ArticleMst.ARTICLE_MST.ID.eq(articleId)) .and(ArticleMst.ARTICLE_MST.DELETED_AT.isNull) .fetchOneInto(SelectArticleRecord::class.java) - ?: throw IllegalArgumentException("cannot find article record by articleId: $articleId") } - fun selectWorkBookArticleRecord(query: SelectWorkBookArticleRecordQuery): SelectWorkBookArticleRecord { + fun selectWorkBookArticleRecord(query: SelectWorkBookArticleRecordQuery): SelectWorkBookArticleRecord? { val articleMst = ArticleMst.ARTICLE_MST val articleIfo = ArticleIfo.ARTICLE_IFO val mappingWorkbookArticle = MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE @@ -62,7 +61,6 @@ class ArticleDao( .where(articleMst.ID.eq(articleId)) .and(articleMst.DELETED_AT.isNull) .fetchOneInto(SelectWorkBookArticleRecord::class.java) - ?: throw IllegalArgumentException("cannot find $workbookId article record by articleId: $articleId") } fun selectWorkbookMappedArticleRecords(query: SelectWorkbookMappedArticleRecordsQuery): List { 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 7d23ae622..c5585d002 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 @@ -4,7 +4,7 @@ import com.few.api.repo.dao.member.command.InsertMemberCommand 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.MemberRecord +import com.few.api.repo.dao.member.record.MemberIdRecord import com.few.api.repo.dao.member.record.WriterRecord import com.few.data.common.code.MemberType import jooq.jooq_dsl.tables.Member @@ -47,17 +47,16 @@ class MemberDao( .fetchInto(WriterRecord::class.java) } - fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberRecord { + fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdRecord? { val email = query.email return dslContext.select( - Member.MEMBER.ID.`as`(MemberRecord::memberId.name) + Member.MEMBER.ID.`as`(MemberIdRecord::memberId.name) ) .from(Member.MEMBER) .where(Member.MEMBER.EMAIL.eq(email)) .and(Member.MEMBER.DELETED_AT.isNull) - .fetchOneInto(MemberRecord::class.java) - ?: throw IllegalArgumentException("cannot find member record by email: $email") + .fetchOneInto(MemberIdRecord::class.java) } fun insertMember(command: InsertMemberCommand): Long { diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt similarity index 71% rename from api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberRecord.kt rename to api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt index b98d9c86f..0786d75d8 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberRecord.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdRecord.kt @@ -1,5 +1,5 @@ package com.few.api.repo.dao.member.record -data class MemberRecord( +data class MemberIdRecord( val memberId: Long ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapper.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapper.kt deleted file mode 100644 index dfebab7f3..000000000 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapper.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.few.api.repo.dao.member.support - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.registerKotlinModule -import org.springframework.stereotype.Component - -@Component -class WriterDescriptionMapper( - private val objectMapper: ObjectMapper -) { - init { - objectMapper.registerKotlinModule() - } - - fun toJson(writerDescription: WriterDescription): String { - return objectMapper.writeValueAsString(writerDescription) - } - - fun toObject(value: String): WriterDescription { - return objectMapper.readValue(value, WriterDescription::class.java) - } -} \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/ProblemDao.kt index b65ed833d..236e78a25 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 @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component class ProblemDao( private val dslContext: DSLContext ) { - fun selectProblemContents(query: SelectProblemQuery): SelectProblemRecord { + fun selectProblemContents(query: SelectProblemQuery): SelectProblemRecord? { return dslContext.select( Problem.PROBLEM.ID.`as`(SelectProblemRecord::id.name), Problem.PROBLEM.TITLE.`as`(SelectProblemRecord::title.name), @@ -26,10 +26,9 @@ class ProblemDao( .where(Problem.PROBLEM.ID.eq(query.problemId)) .and(Problem.PROBLEM.DELETED_AT.isNull) .fetchOneInto(SelectProblemRecord::class.java) - ?: throw RuntimeException("Problem with ID ${query.problemId} not found") // TODO: 에러 표준화 } - fun selectProblemAnswer(query: SelectProblemAnswerQuery): SelectProblemAnswerRecord { + fun selectProblemAnswer(query: SelectProblemAnswerQuery): SelectProblemAnswerRecord? { return dslContext.select( Problem.PROBLEM.ID.`as`(SelectProblemAnswerRecord::id.name), Problem.PROBLEM.ANSWER.`as`(SelectProblemAnswerRecord::answer.name), @@ -39,10 +38,9 @@ class ProblemDao( .where(Problem.PROBLEM.ID.eq(query.problemId)) .and(Problem.PROBLEM.DELETED_AT.isNull) .fetchOneInto(SelectProblemAnswerRecord::class.java) - ?: throw RuntimeException("Problem Answer with ID ${query.problemId} not found") // TODO: 에러 표준화 } - fun selectProblemsByArticleId(query: SelectProblemsByArticleIdQuery): ProblemIdsRecord { + fun selectProblemsByArticleId(query: SelectProblemsByArticleIdQuery): ProblemIdsRecord? { val articleId = query.articleId return dslContext.select() diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapper.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapper.kt index dba9e8b07..1cb22ae52 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapper.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapper.kt @@ -16,7 +16,7 @@ class ContentsJsonMapper( val contents = objectMapper.readTree(value).get("contents") val contentList = mutableListOf() contents.forEach { - contentList.add(Content(it.get("id").asLong(), it.get("content").asText())) + contentList.add(Content(it.get("number").asLong(), it.get("content").asText())) } return Contents(contentList) } diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsMapper.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsMapper.kt deleted file mode 100644 index f86b4579f..000000000 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/problem/support/ContentsMapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.few.api.repo.dao.problem.support - -import com.fasterxml.jackson.databind.ObjectMapper -import org.springframework.stereotype.Component - -@Component -class ContentsMapper( - private val objectMapper: ObjectMapper -) { - - fun toJson(contents: Contents): String { - return objectMapper.writeValueAsString(contents) - } - - fun toObject(value: String): Contents { - val contents = objectMapper.readTree(value).get("contents") - val contentList = mutableListOf() - contents.forEach { - contentList.add(Content(it.get("id").asLong(), it.get("content").asText())) - } - return Contents(contentList) - } -} \ 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 4e0d5fd2a..bd81c9c5a 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 @@ -10,7 +10,7 @@ import org.springframework.stereotype.Repository class WorkbookDao( private val dslContext: DSLContext ) { - fun selectWorkBook(query: SelectWorkBookRecordQuery): SelectWorkBookRecord { + fun selectWorkBook(query: SelectWorkBookRecordQuery): SelectWorkBookRecord? { return dslContext.select( Workbook.WORKBOOK.ID.`as`(SelectWorkBookRecord::id.name), Workbook.WORKBOOK.TITLE.`as`(SelectWorkBookRecord::title.name), @@ -23,6 +23,5 @@ class WorkbookDao( .where(Workbook.WORKBOOK.ID.eq(query.id)) .and(Workbook.WORKBOOK.DELETED_AT.isNull) .fetchOneInto(SelectWorkBookRecord::class.java) - ?: throw RuntimeException("WorkBook with ID ${query.id} not found") } } \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/article/ArticleDaoTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/article/ArticleDaoTest.kt index 3a34032ca..72b78e483 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/article/ArticleDaoTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/article/ArticleDaoTest.kt @@ -57,7 +57,7 @@ class ArticleDaoTest : JooqTestSpec() { } // then - assertNotNull(result) + assertNotNull(result!!) assertEquals(1L, result.articleId) assertEquals(1L, result.writerId) assertEquals(URL("http://localhost:8080/image1.jpg"), result.mainImageURL) @@ -79,7 +79,7 @@ class ArticleDaoTest : JooqTestSpec() { } // then - assertNotNull(result) + assertNotNull(result!!) assertEquals(1L, result.articleId) assertEquals(1L, result.writerId) assertEquals(URL("http://localhost:8080/image1.jpg"), result.mainImageURL) diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapperTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapperTest.kt deleted file mode 100644 index 49bc60a4f..000000000 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/WriterDescriptionMapperTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.few.api.repo.dao.member.support - -import com.fasterxml.jackson.databind.ObjectMapper -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest - -import java.net.URL - -@SpringBootTest(classes = [ObjectMapper::class]) -class WriterDescriptionMapperTest { - - @Autowired - private lateinit var objectMapper: ObjectMapper - private lateinit var writerDescriptionMapper: WriterDescriptionMapper - - @BeforeEach - fun setUp() { - writerDescriptionMapper = WriterDescriptionMapper(objectMapper) - } - - @Test - fun `WriterDescription을 Json 형식으로 변환합니다`() { - // Given - val writerDescription = WriterDescription( - name = "writer", - url = URL("http://localhost:8080/writers/url") - ) - - // When - val json = writerDescriptionMapper.toJson(writerDescription) - - // Then - assertNotNull(json) - assertTrue(json.isNotBlank()) - assertTrue(json.contains("writer")) - assertTrue(json.contains("http://localhost:8080/writers/url")) - } - - @Test - fun `Json 형식의 WriterDescription을 WriterDescription으로 변환합니다`() { - // Given - val json = """ - { - "name": "writer", - "url": "http://localhost:8080/writers/url" - } - """.trimIndent() - - // When - val writerDescription = writerDescriptionMapper.toObject(json) - - // Then - assertNotNull(writerDescription) - assertEquals("writer", writerDescription.name) - assertEquals(URL("http://localhost:8080/writers/url"), writerDescription.url) - } -} \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/ProblemDaoTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/ProblemDaoTest.kt index aca57429f..b0ecb0315 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/ProblemDaoTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/ProblemDaoTest.kt @@ -59,7 +59,7 @@ class ProblemDaoTest : JooqTestSpec() { val result = problemDao.selectProblemsByArticleId(query) // then - assertNotNull(result) + assertNotNull(result!!) assertEquals(1, result.problemIds.size) assertEquals(1, result.problemIds[0]) } diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapperTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapperTest.kt index fd3fa011b..8e6cbc754 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapperTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsJsonMapperTest.kt @@ -35,11 +35,11 @@ class ContentsJsonMapperTest { { "contents": [ { - "id": 1, + "number": 1, "content": "this is number one" }, { - "id": 2, + "number": 2, "content": "this is number two" } ] diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsMapperTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsMapperTest.kt deleted file mode 100644 index 3f9866dba..000000000 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/problem/support/ContentsMapperTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.few.api.repo.dao.problem.support - -import com.fasterxml.jackson.databind.ObjectMapper -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest(classes = [ObjectMapper::class]) -class ContentsMapperTest { - - @Autowired - private lateinit var objectMapper: ObjectMapper - - private lateinit var contentsMapper: ContentsMapper - - @BeforeEach - fun setUp() { - contentsMapper = ContentsMapper(objectMapper) - } - - @Test - fun `Contents를 Json 형식으로 변환합니다`() { - // Given - val contents = Contents( - contents = listOf( - Content(1L, "this is number one"), - Content(2L, "this is number two") - ) - ) - - // When - val json = contentsMapper.toJson(contents) - - // Then - assertNotNull(json) - assertTrue(json.isNotBlank()) - assertTrue(json.contains("this is number one")) - assertTrue(json.contains("this is number two")) - } - - @Test - fun `Json 형식의 Contents를 Contents으로 변환합니다`() { - // Given - val json = """ - { - "contents": [ - { - "id": 1, - "content": "this is number one" - }, - { - "id": 2, - "content": "this is number two" - } - ] - } - """.trimIndent() - - // When - val contents = contentsMapper.toObject(json) - - // Then - assertNotNull(contents) - assertEquals(2, contents.contents.size) - assertEquals(1L, contents.contents[0].number) - assertEquals("this is number one", contents.contents[0].content) - assertEquals(2L, contents.contents[1].number) - assertEquals("this is number two", contents.contents[1].content) - } -} \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/workbook/WorkbookDaoTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/workbook/WorkbookDaoTest.kt index 2f5554622..a1ac6c8fa 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/workbook/WorkbookDaoTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/workbook/WorkbookDaoTest.kt @@ -41,7 +41,7 @@ class WorkbookDaoTest : JooqTestSpec() { } // then - assertNotNull(result) + assertNotNull(result!!) assertEquals(1L, result.id) assertEquals("title1", result.title) assertEquals(URL("http://localhost:8080/image1.jpg"), result.mainImageUrl) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 3ee3f221c..d2a00faa0 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,4 +1,5 @@ import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI +import java.util.Random dependencies { /** module */ @@ -6,7 +7,7 @@ dependencies { implementation(project(":batch")) /** spring starter */ - implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-actuator") /** swagger & restdocs */ @@ -25,9 +26,17 @@ plugins { id("org.hidetake.swagger.generator") version DependencyVersion.SWAGGER_GENERATOR } +val serverUrl = project.hasProperty("serverUrl").let { + if (it) { + project.property("serverUrl") as String + } else { + "http://localhost:8080" + } +} + /** convert snippet to swagger */ openapi3 { - this.setServer("http://localhost:8080") // todo refactor to use setServers + this.setServer(serverUrl) title = project.name version = project.version.toString() format = "yaml" @@ -40,24 +49,29 @@ openapi3 { postman { title = project.name version = project.version.toString() - baseUrl = "http://localhost:8080" + baseUrl = serverUrl outputDirectory = "src/main/resources/static/" outputFileNamePrefix = "postman" } +/** generate swagger ui */ swaggerSources { + /** generateSwaggerUIApi */ register("api") { setInputFile(file("$projectDir/src/main/resources/static/openapi3.yaml")) } } -tasks.withType(GenerateSwaggerUI::class) { - dependsOn("openapi3") -} - -tasks.register("generateApiSwaggerUI", Copy::class) { - dependsOn("generateSwaggerUI") +/** + * generate static swagger ui
+ * need snippet to generate swagger ui + * */ +tasks.register("generateStaticSwaggerUIApi", Copy::class) { + /** generateSwaggerUIApi */ + dependsOn("generateSwaggerUIApi") val generateSwaggerUISampleTask = tasks.named("generateSwaggerUIApi", GenerateSwaggerUI::class).get() + + /** copy */ from(generateSwaggerUISampleTask.outputDir) into("$projectDir/src/main/resources/static/docs/swagger-ui") } @@ -66,20 +80,19 @@ val imageName = project.hasProperty("imageName").let { if (it) { project.property("imageName") as String } else { - "api" + "fewletter/api" } } val releaseVersion = project.hasProperty("releaseVersion").let { if (it) { project.property("releaseVersion") as String } else { - "latest" + Random().nextInt(90000) + 10000 } } tasks.register("buildDockerImage") { dependsOn("bootJar") - dependsOn("generateApiSwaggerUI") doLast { exec { @@ -94,7 +107,18 @@ tasks.register("buildDockerImage") { exec { workingDir(".") - commandLine("docker", "buildx", "build", "--platform=linux/amd64,linux/arm64", "-t", "fewletter/$imageName", "--build-arg", "RELEASE_VERSION=$releaseVersion", ".", "--push") + commandLine( + "docker", "buildx", "build", "--platform=linux/amd64,linux/arm64", "-t", + "$imageName:latest", "--build-arg", "RELEASE_VERSION=$releaseVersion", ".", "--push" + ) + } + + exec { + workingDir(".") + commandLine( + "docker", "buildx", "build", "--platform=linux/amd64,linux/arm64", "-t", + "$imageName:$releaseVersion", "--build-arg", "RELEASE_VERSION=$releaseVersion", ".", "--push" + ) } } } \ 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 b23625075..01ede359b 100644 --- a/api/src/main/kotlin/com/few/api/config/ApiConfig.kt +++ b/api/src/main/kotlin/com/few/api/config/ApiConfig.kt @@ -5,12 +5,10 @@ import com.few.batch.config.BatchConfig import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import -import org.springframework.web.servlet.config.annotation.EnableWebMvc @Configuration @ComponentScan(basePackages = [ApiConfig.BASE_PACKAGE]) @Import(ApiRepoConfig::class, BatchConfig::class) -@EnableWebMvc class ApiConfig { companion object { const val BASE_PACKAGE = "com.few.api" 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 83d3b745c..744f50121 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 @@ -13,7 +13,7 @@ class BrowseArticleProblemsService( fun execute(query: BrowseArticleProblemIdsQuery): ProblemIdsRecord { SelectProblemsByArticleIdQuery(query.articleId).let { query: SelectProblemsByArticleIdQuery -> - return problemDao.selectProblemsByArticleId(query) + return problemDao.selectProblemsByArticleId(query) ?: throw IllegalArgumentException("cannot find problems by articleId: ${query.articleId}") // todo 에러 표준화 } } } \ No newline at end of file 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 cdcfca55b..18dcab29f 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 @@ -23,7 +23,7 @@ class ReadArticleUseCase( fun execute(useCaseIn: ReadArticleUseCaseIn): ReadArticleUseCaseOut { val articleRecord = SelectArticleRecordQuery(useCaseIn.articleId).let { query: SelectArticleRecordQuery -> articleDao.selectArticleRecord(query) - } + } ?: throw IllegalArgumentException("cannot find article record by articleId: $useCaseIn.articleId") val writerRecord = ReadWriterRecordQuery(articleRecord.writerId).let { query: ReadWriterRecordQuery -> readArticleWriterRecordService.execute(query) 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 16c65a7e8..96f66e7fb 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 @@ -20,8 +20,8 @@ class CheckProblemUseCase( val problemId = useCaseIn.problemId val submitAns = useCaseIn.sub - val record = problemDao.selectProblemAnswer(SelectProblemAnswerQuery(problemId)) - val isSolved = submitAns.equals(record.answer) + val record = problemDao.selectProblemAnswer(SelectProblemAnswerQuery(problemId)) ?: throw RuntimeException("Problem Answer with ID $problemId not found") // TODO: 에러 표준화 + val isSolved = record.answer == submitAns val submitHistoryId = submitHistoryDao.insertSubmitHistory( InsertSubmitHistoryCommand(problemId, 1L, submitAns, isSolved) diff --git a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt index 56a8ce042..a66019754 100644 --- a/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCase.kt @@ -1,19 +1,18 @@ package com.few.api.domain.problem.usecase -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue import com.few.api.repo.dao.problem.ProblemDao import com.few.api.repo.dao.problem.query.SelectProblemQuery import com.few.api.domain.problem.usecase.`in`.ReadProblemUseCaseIn import com.few.api.domain.problem.usecase.out.ReadProblemContentsUseCaseOutDetail import com.few.api.domain.problem.usecase.out.ReadProblemUseCaseOut +import com.few.api.repo.dao.problem.support.ContentsJsonMapper import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @Component class ReadProblemUseCase( private val problemDao: ProblemDao, - private val objectMapper: ObjectMapper + private val contentsJsonMapper: ContentsJsonMapper ) { @Transactional(readOnly = true) @@ -21,8 +20,14 @@ class ReadProblemUseCase( val problemId = useCaseIn.problemId val record = problemDao.selectProblemContents(SelectProblemQuery(problemId)) + ?: throw RuntimeException("Problem with ID $problemId not found") // TODO: 에러 표준화 - val contents: List = objectMapper.readValue(record.contents) + val contents: List = contentsJsonMapper.toObject(record.contents).contents.map { + ReadProblemContentsUseCaseOutDetail( + number = it.number, + content = it.content + ) + } return ReadProblemUseCaseOut( id = record.id, diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/service/MemberService.kt b/api/src/main/kotlin/com/few/api/domain/subscription/service/MemberService.kt index aa65fc19a..90e95c58b 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/service/MemberService.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/service/MemberService.kt @@ -5,6 +5,7 @@ import com.few.api.domain.subscription.service.dto.ReadMemberIdDto import com.few.api.repo.dao.member.MemberDao import com.few.api.repo.dao.member.command.InsertMemberCommand import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery +import com.few.api.repo.dao.member.record.MemberIdRecord import org.springframework.stereotype.Service @Service @@ -12,8 +13,8 @@ class MemberService( private val memberDao: MemberDao ) { - fun readMemberId(dto: ReadMemberIdDto): Long? { - return memberDao.selectMemberByEmail(SelectMemberByEmailQuery(dto.email)).memberId + fun readMemberId(dto: ReadMemberIdDto): MemberIdRecord? { + return memberDao.selectMemberByEmail(SelectMemberByEmailQuery(dto.email)) } fun insertMember(dto: InsertMemberDto): Long { 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 ac0249372..716e67cbe 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 @@ -17,12 +17,13 @@ class SubscribeWorkbookUseCase( private val memberService: MemberService ) { + // todo 이미 가입된 경우 @Transactional fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) { // TODO: request sending email - val memberId = memberService.readMemberId(ReadMemberIdDto(useCaseIn.email)) ?: memberService.insertMember( - InsertMemberDto(useCaseIn.email, MemberType.NORMAL) + val memberId = memberService.readMemberId(ReadMemberIdDto(useCaseIn.email))?.memberId ?: memberService.insertMember( + InsertMemberDto(email = useCaseIn.email, memberType = MemberType.NORMAL) ) // 이미 구독중인지 확인 diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt index b198f94b7..b96ef291c 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt @@ -19,7 +19,7 @@ class UnsubscribeAllUseCase( // TODO: request sending email val memberId = - memberService.readMemberId(ReadMemberIdDto(useCaseIn.email)) ?: throw RuntimeException("Member Not Found") + memberService.readMemberId(ReadMemberIdDto(useCaseIn.email))?.memberId ?: throw RuntimeException("Not found member") subscriptionDao.updateDeletedAtInAllSubscription( UpdateDeletedAtInAllSubscriptionCommand(memberId = memberId, opinion = useCaseIn.opinion) diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt index d59444fbe..1e2fc8be0 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt @@ -19,7 +19,7 @@ class UnsubscribeWorkbookUseCase( // TODO: request sending email val memberId = - memberService.readMemberId(ReadMemberIdDto(useCaseIn.email)) ?: throw RuntimeException("Member Not Found") + memberService.readMemberId(ReadMemberIdDto(useCaseIn.email))?.memberId ?: throw RuntimeException("Not found member") subscriptionDao.updateDeletedAtInWorkbookSubscription( UpdateDeletedAtInWorkbookSubscriptionCommand( 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 1b882d750..079c91357 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 @@ -25,7 +25,7 @@ class ReadWorkBookArticleUseCase( useCaseIn.articleId ).let { query: SelectWorkBookArticleRecordQuery -> articleDao.selectWorkBookArticleRecord(query) - } + } ?: throw IllegalArgumentException("cannot find $useCaseIn.workbookId article record by articleId: $useCaseIn.articleId") val writerRecord = ReadWriterRecordQuery(articleRecord.writerId).let { query: ReadWriterRecordQuery -> 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 29d4fafa0..83c8f8613 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 @@ -20,7 +20,7 @@ class ReadWorkbookUseCase( val workbookId = useCaseIn.workbookId val workbookRecord = SelectWorkBookRecordQuery(workbookId).let { query -> - workbookDao.selectWorkBook(query) + workbookDao.selectWorkBook(query) ?: throw RuntimeException("WorkBook with ID ${query.id} not found") } val workbookMappedArticles = BrowseWorkbookArticlesQuery(workbookId).let { query -> 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 new file mode 100644 index 000000000..cb308f31c --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt @@ -0,0 +1,26 @@ +package com.few.api.web.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOriginPatterns(CorsConfiguration.ALL) + .allowedMethods(CorsConfiguration.ALL) + .allowedHeaders(CorsConfiguration.ALL) + .allowCredentials(true) + .maxAge(3600) + } + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/**") + .addResourceLocations("classpath:/static/") + } +} \ No newline at end of file diff --git a/api/src/main/resources/application.yml b/api/src/main/resources/application.yml index 63a505873..0ced4f5db 100644 --- a/api/src/main/resources/application.yml +++ b/api/src/main/resources/application.yml @@ -10,3 +10,6 @@ spring: prd: - api-repo-prd - email-prd +logging: + level: + root: debug diff --git a/batch/build.gradle.kts b/batch/build.gradle.kts index ebd32850d..4fb79680d 100644 --- a/batch/build.gradle.kts +++ b/batch/build.gradle.kts @@ -6,21 +6,6 @@ tasks.getByName("jar") { enabled = true } -plugins { - /** jooq */ - id("org.jooq.jooq-codegen-gradle") version DependencyVersion.JOOQ -} - -sourceSets { - main { - java { - val mainDir = "src/main/kotlin" - val jooqDir = "src/generated" - srcDirs(mainDir, jooqDir) - } - } -} - dependencies { /** module */ implementation(project(":email")) @@ -46,100 +31,4 @@ dependencies { /** test flyway */ testImplementation("org.flywaydb:flyway-core:${DependencyVersion.FLYWAY}") testImplementation("org.flywaydb:flyway-mysql") -} - -/** copy data migration */ -tasks.create("copyDataMigration") { - doLast { - val root = rootDir - val flyWayResourceDir = "/db/migration/entity" - val dataMigrationDir = "$root/data/$flyWayResourceDir" - File(dataMigrationDir).walkTopDown().forEach { - if (it.isFile) { - it.copyTo( - File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), - true - ) - } - } - } -} - -/** copy data migration before compile kotlin */ -tasks.getByName("compileKotlin") { - dependsOn("copyDataMigration") -} - -/** jooq codegen after copy data migration */ -tasks.getByName("jooqCodegen") { - dependsOn("copyDataMigration") -} - -jooq { - configuration { - generator { - database { - name = "org.jooq.meta.extensions.ddl.DDLDatabase" - properties { - // Specify the location of your SQL script. - // You may use ant-style file matching, e.g. /path/**/to/*.sql - // - // Where: - // - ** matches any directory subtree - // - * matches any number of characters in a directory / file name - // - ? matches a single character in a directory / file name - property { - key = "scripts" - value = "src/main/resources/db/migration/**/*.sql" - } - - // The sort order of the scripts within a directory, where: - // - // - semantic: sorts versions, e.g. v-3.10.0 is after v-3.9.0 (default) - // - alphanumeric: sorts strings, e.g. v-3.10.0 is before v-3.9.0 - // - flyway: sorts files the same way as flyway does - // - none: doesn't sort directory contents after fetching them from the directory - property { - key = "sort" - value = "flyway" - } - - // The default schema for unqualified objects: - // - // - public: all unqualified objects are located in the PUBLIC (upper case) schema - // - none: all unqualified objects are located in the default schema (default) - // - // This configuration can be overridden with the schema mapping feature - property { - key = "unqualifiedSchema" - value = "none" - } - - // The default name case for unquoted objects: - // - // - as_is: unquoted object names are kept unquoted - // - upper: unquoted object names are turned into upper case (most databases) - // - lower: unquoted object names are turned into lower case (e.g. PostgreSQL) - property { - key = "defaultNameCase" - value = "as_is" - } - } - } - - generate { - isDeprecated = false - isRecords = true - isImmutablePojos = true - isFluentSetters = true - isJavaTimeTypes = true - } - - target { - packageName = "jooq.jooq_dsl" - directory = "src/generated" - encoding = "UTF-8" - } - } - } } \ No newline at end of file diff --git a/batch/src/main/kotlin/com/few/batch/service/article/BatchSendArticleEmailService.kt b/batch/src/main/kotlin/com/few/batch/service/article/BatchSendArticleEmailService.kt index f96daa50e..bffca33e0 100644 --- a/batch/src/main/kotlin/com/few/batch/service/article/BatchSendArticleEmailService.kt +++ b/batch/src/main/kotlin/com/few/batch/service/article/BatchSendArticleEmailService.kt @@ -16,13 +16,17 @@ class BatchSendArticleEmailService( ) { @Transactional fun execute() { + val startTime = System.currentTimeMillis() workBookSubscriberReader.execute().let { item -> - workBookSubscriberWriter.execute(item).let { execution -> - objectMapper.writeValueAsString(execution).let { json -> - if (!json.contains("fail")) { - batchCallExecutionService.execute(false, json) - } else { - batchCallExecutionService.execute(true, json) + workBookSubscriberWriter.execute(item).let { resultExecution -> + val elapsedTime = System.currentTimeMillis() - startTime + resultExecution.plus("elapsedTime" to elapsedTime).let { execution -> + objectMapper.writeValueAsString(execution).let { json -> + if (!json.contains("fail")) { + batchCallExecutionService.execute(true, json) + } else { + batchCallExecutionService.execute(false, json) + } } } } diff --git a/build.gradle.kts b/build.gradle.kts index 24bab4203..70be90d5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,9 @@ plugins { id("org.springframework.boot") version DependencyVersion.SPRING_BOOT id("io.spring.dependency-management") version DependencyVersion.SPRING_DEPENDENCY_MANAGEMENT + /** jooq */ + id("org.jooq.jooq-codegen-gradle") version DependencyVersion.JOOQ + /** ktlint */ id("org.jlleitschuh.gradle.ktlint") version DependencyVersion.KTLINT @@ -44,6 +47,16 @@ allprojects { tasks.withType { useJUnitPlatform() } + + sourceSets { + main { + java { + val mainDir = "src/main/kotlin" + val jooqDir = "src/generated" + srcDirs(mainDir, jooqDir) + } + } + } } tasks.getByName("bootJar") { @@ -57,6 +70,7 @@ subprojects { apply(plugin = "org.jetbrains.kotlin.kapt") apply(plugin = "org.jlleitschuh.gradle.ktlint") apply(plugin = "org.hidetake.swagger.generator") + apply(plugin = "org.jooq.jooq-codegen-gradle") /** * https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support @@ -80,6 +94,13 @@ subprojects { implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + /** jooq */ + implementation("org.springframework.boot:spring-boot-starter-jooq") + implementation("org.jooq:jooq:${DependencyVersion.JOOQ}") + implementation("org.jooq:jooq-meta:${DependencyVersion.JOOQ}") + implementation("org.jooq:jooq-codegen:${DependencyVersion.JOOQ}") + jooqCodegen("org.jooq:jooq-meta-extensions:${DependencyVersion.JOOQ}") + /** test **/ testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("io.mockk:mockk:${DependencyVersion.MOCKK}") @@ -94,9 +115,112 @@ subprojects { swaggerUI("org.webjars:swagger-ui:${DependencyVersion.SWAGGER_UI}") } + /** copy data migration */ + tasks.create("copyDataMigration") { + doLast { + val root = rootDir + val flyWayResourceDir = "/db/migration/entity" + val dataMigrationDir = "$root/data/$flyWayResourceDir" + File(dataMigrationDir).walkTopDown().forEach { + if (it.isFile) { + it.copyTo( + File("${project.projectDir}/src/main/resources$flyWayResourceDir/${it.name}"), + true + ) + } + } + } + } + + /** copy data migration before compile kotlin */ + tasks.getByName("compileKotlin") { + dependsOn("copyDataMigration") + } + + /** jooq codegen after copy data migration */ + tasks.getByName("jooqCodegen") { + dependsOn("copyDataMigration") + } + + jooq { + configuration { + generator { + database { + name = "org.jooq.meta.extensions.ddl.DDLDatabase" + properties { + // Specify the location of your SQL script. + // You may use ant-style file matching, e.g. /path/**/to/*.sql + // + // Where: + // - ** matches any directory subtree + // - * matches any number of characters in a directory / file name + // - ? matches a single character in a directory / file name + property { + key = "scripts" + value = "src/main/resources/db/migration/**/*.sql" + } + + // The sort order of the scripts within a directory, where: + // + // - semantic: sorts versions, e.g. v-3.10.0 is after v-3.9.0 (default) + // - alphanumeric: sorts strings, e.g. v-3.10.0 is before v-3.9.0 + // - flyway: sorts files the same way as flyway does + // - none: doesn't sort directory contents after fetching them from the directory + property { + key = "sort" + value = "flyway" + } + + // The default schema for unqualified objects: + // + // - public: all unqualified objects are located in the PUBLIC (upper case) schema + // - none: all unqualified objects are located in the default schema (default) + // + // This configuration can be overridden with the schema mapping feature + property { + key = "unqualifiedSchema" + value = "none" + } + + // The default name case for unquoted objects: + // + // - as_is: unquoted object names are kept unquoted + // - upper: unquoted object names are turned into upper case (most databases) + // - lower: unquoted object names are turned into lower case (e.g. PostgreSQL) + property { + key = "defaultNameCase" + value = "as_is" + } + } + } + + generate { + isDeprecated = false + isRecords = true + isImmutablePojos = true + isFluentSetters = true + isJavaTimeTypes = true + } + + target { + packageName = "jooq.jooq_dsl" + directory = "src/generated" + encoding = "UTF-8" + } + } + } + } + defaultTasks("bootRun") } +/** do all jooq codegen */ +tasks.register("jooqCodegenAll") { + dependsOn(":api:jooqCodegen") + dependsOn(":api-repo:jooqCodegen") + dependsOn(":batch:jooqCodegen") +} + /** git hooks */ tasks.register("gitExecutableHooks") { doLast { diff --git a/data/db/migration/entity/V1.00.0.0__draft_table_design.sql b/data/db/migration/entity/V1.00.0.0__draft_table_design.sql index f4fe58634..6256ffc37 100644 --- a/data/db/migration/entity/V1.00.0.0__draft_table_design.sql +++ b/data/db/migration/entity/V1.00.0.0__draft_table_design.sql @@ -1,5 +1,5 @@ -- 작가 및 유저 -CREATE TABLE member +CREATE TABLE MEMBER ( id BIGINT NOT NULL AUTO_INCREMENT, email VARCHAR(255) NOT NULL, @@ -13,7 +13,7 @@ CREATE TABLE member ); -- 아티클 마스터 -CREATE TABLE article_mst +CREATE TABLE ARTICLE_MST ( id BIGINT NOT NULL AUTO_INCREMENT, member_id BIGINT NOT NULL, @@ -26,7 +26,7 @@ CREATE TABLE article_mst ); -- 아티클 인포 -CREATE TABLE article_ifo +CREATE TABLE ARTICLE_IFO ( article_mst_id BIGINT NOT NULL, content TEXT NOT NULL, @@ -35,7 +35,7 @@ CREATE TABLE article_ifo ); -- 학습지 -CREATE TABLE workbook +CREATE TABLE WORKBOOK ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, @@ -48,7 +48,7 @@ CREATE TABLE workbook ); -- 작가-학습지 매핑테이블(다대다) -CREATE TABLE mapping_member_workbook +CREATE TABLE MAPPING_MEMBER_WORKBOOK ( member_id BIGINT NOT NULL, workbook_id BIGINT NOT NULL, @@ -57,7 +57,7 @@ CREATE TABLE mapping_member_workbook ); -- 학습지-아티클 매핑테이블(다대다) -CREATE TABLE mapping_workbook_article +CREATE TABLE MAPPING_WORKBOOK_ARTICLE ( workbook_id BIGINT NOT NULL, article_id BIGINT NOT NULL, @@ -67,7 +67,7 @@ CREATE TABLE mapping_workbook_article ); -- 문제, 정답, 해설(메타데이터) -CREATE TABLE problem +CREATE TABLE PROBLEM ( id BIGINT NOT NULL AUTO_INCREMENT, article_id BIGINT NOT NULL, @@ -82,7 +82,7 @@ CREATE TABLE problem ); -- 풀이 히스토리 -CREATE TABLE submit_history +CREATE TABLE SUBMIT_HISTORY ( id BIGINT NOT NULL AUTO_INCREMENT, problem_id BIGINT NOT NULL, @@ -95,7 +95,7 @@ CREATE TABLE submit_history ); -- 구독 -CREATE TABLE subscription +CREATE TABLE SUBSCRIPTION ( id BIGINT NOT NULL AUTO_INCREMENT, member_id BIGINT NOT NULL, @@ -110,7 +110,7 @@ CREATE TABLE subscription -- [인덱스 추가] -- -- problem_idx1: problem 테이블에서 article_id 기반으로 문제 조회시 사용 -CREATE INDEX problem_idx1 ON problem (article_id); +CREATE INDEX problem_idx1 ON PROBLEM (article_id); -- article_mst_idx1: 작가가 작성한 아티클 조회시 사용 -CREATE INDEX article_mst_idx1 ON article_mst (member_id); +CREATE INDEX article_mst_idx1 ON ARTICLE_MST (member_id); diff --git a/data/db/migration/entity/V1.00.0.1__add_column.sql b/data/db/migration/entity/V1.00.0.1__add_column.sql index 6f775d87f..c9f1ce20e 100644 --- a/data/db/migration/entity/V1.00.0.1__add_column.sql +++ b/data/db/migration/entity/V1.00.0.1__add_column.sql @@ -1,3 +1,3 @@ -- subscription 테이블에 reason 컬럼 추가 (한국어로만 100자 가능) -ALTER TABLE subscription +ALTER TABLE SUBSCRIPTION ADD COLUMN unsubs_opinion VARCHAR(300) NULL; diff --git a/data/db/migration/entity/V1.00.0.2__add_subscription_progress.sql b/data/db/migration/entity/V1.00.0.2__add_subscription_progress.sql index 60e206295..3f5b0941c 100644 --- a/data/db/migration/entity/V1.00.0.2__add_subscription_progress.sql +++ b/data/db/migration/entity/V1.00.0.2__add_subscription_progress.sql @@ -1,2 +1,2 @@ -- 구독 진행 사항 컬럼 추가 -ALTER TABLE subscription ADD COLUMN progress BIGINT NOT NULL DEFAULT 0; +ALTER TABLE SUBSCRIPTION ADD COLUMN progress BIGINT NOT NULL DEFAULT 0; diff --git a/data/db/migration/entity/V1.00.0.3__batch_call_execution_table.sql b/data/db/migration/entity/V1.00.0.3__batch_call_execution_table.sql index 8bbb3a519..fbf522bd5 100644 --- a/data/db/migration/entity/V1.00.0.3__batch_call_execution_table.sql +++ b/data/db/migration/entity/V1.00.0.3__batch_call_execution_table.sql @@ -1,5 +1,5 @@ -- 배치 요청 기록 테이블 -CREATE TABLE batch_call_execution +CREATE TABLE BATCH_CALL_EXECUTION ( id BIGINT PRIMARY KEY AUTO_INCREMENT, status bit(1) NOT NULL, diff --git a/email/src/main/resources/application-email-local.yml b/email/src/main/resources/application-email-local.yml index a6e7c4dbb..471d8d553 100644 --- a/email/src/main/resources/application-email-local.yml +++ b/email/src/main/resources/application-email-local.yml @@ -4,7 +4,7 @@ spring: host: smtp.gmail.com port: 587 username: DevFewFew@gmail.com - password: ${EMAIL_PASSWORD} + password: ${EMAIL_PASSWORD:password} properties: mail: smtp: diff --git a/email/src/test/resources/application-test.yml b/email/src/test/resources/application-test.yml index 83b9b042b..06f5a2520 100644 --- a/email/src/test/resources/application-test.yml +++ b/email/src/test/resources/application-test.yml @@ -4,7 +4,7 @@ spring: protocol: smtp port: 25 username: 36a1889f02050c - password: ${MAIL_PASSWORD} + password: ${EMAIL_PASSWORD} properties: mail: smtp: