diff --git a/.github/workflows/code-ci.yml b/.github/workflows/code-ci.yml index 81248cb19..079f69e00 100644 --- a/.github/workflows/code-ci.yml +++ b/.github/workflows/code-ci.yml @@ -17,6 +17,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/ecs-cd.yml b/.github/workflows/ecs-cd.yml index 57f179f52..ec50581ef 100644 --- a/.github/workflows/ecs-cd.yml +++ b/.github/workflows/ecs-cd.yml @@ -21,6 +21,16 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/ecs-dev-cd.yml b/.github/workflows/ecs-dev-cd.yml new file mode 100644 index 000000000..be0cab2e5 --- /dev/null +++ b/.github/workflows/ecs-dev-cd.yml @@ -0,0 +1,105 @@ +name: DEV - Deploy to Amazon ECS DEV + +on: + push: + branches: + - dev + workflow_dispatch: +env: + AWS_REGION: ap-northeast-2 + ECR_REPOSITORY: few-ecr + ECS_SERVICE: few-ecs-service + ECS_CLUSTER: few-ecs-cluster + ECS_TASK_DEFINITION: task-definition.json + TASK_DEFINITION_NAME: few-ecs-task + CONTAINER_NAME: few-container + +jobs: + build-and-push-docker-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.DEV_AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Jooq Code Generation + run: | + ./gradlew jooqCodegenAll + + - name: Test with Gradle + run: | + ./gradlew test + + - name: Generate OpenAPI3 + run: | + ./gradlew --info api:openapi3 -PserverUrl=https://api.fewletter.shop + + - name: Generate Swagger + run: | + ./gradlew --info api:generateStaticSwaggerUIApi + + - name: Build with Gradle bootBuildImage, tag, and push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + GIT_HASH=$(git rev-parse --short HEAD) + ./gradlew buildEcsDockerImage -PimageName=${ECR_REGISTRY}/${ECR_REPOSITORY}:latest + docker tag ${ECR_REGISTRY}/${ECR_REPOSITORY}:latest ${ECR_REGISTRY}/${ECR_REPOSITORY}:${GIT_HASH} + docker push ${ECR_REGISTRY}/${ECR_REPOSITORY} --all-tags + - name: Get ECR Repository image path + id: get-docker-image-path + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + run: | + GIT_HASH=$(git rev-parse --short HEAD) + echo ${ECR_REGISTRY}/${ECR_REPOSITORY}:${GIT_HASH} + echo "::set-output name=image::${ECR_REGISTRY}/${ECR_REPOSITORY}:${GIT_HASH}" + - name: Download task definition + run: | + aws ecs describe-task-definition --task-definition ${TASK_DEFINITION_NAME} --query taskDefinition > task-definition.json + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-definition + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ env.ECS_TASK_DEFINITION }} + container-name: ${{ env.CONTAINER_NAME }} + image: ${{ steps.get-docker-image-path.outputs.image }} + + - name: Deploy Amazon ECS task definition + id: ecs-deployment + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-definition.outputs.task-definition }} + service: ${{ env.ECS_SERVICE }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true diff --git a/.github/workflows/sql-explain-hook.yml b/.github/workflows/sql-explain-hook.yml index e8de84a17..6fdd260fb 100644 --- a/.github/workflows/sql-explain-hook.yml +++ b/.github/workflows/sql-explain-hook.yml @@ -2,7 +2,8 @@ name: Sql Explain Hook on: pull_request: - branches: ["main"] + types: [ opened ] + branches: [ "main", "dev" ] workflow_dispatch: env: @@ -14,12 +15,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: "17" distribution: "temurin" + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Jooq Code Generation run: | ./gradlew --info jooqCodegenAll diff --git a/.github/workflows/integration-test.yml b/.github/workflows/validate-test.yml similarity index 59% rename from .github/workflows/integration-test.yml rename to .github/workflows/validate-test.yml index 2197ec7ef..4e605d857 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/validate-test.yml @@ -1,8 +1,8 @@ -name: Integration Test +name: Validate Test on: pull_request: - branches: [ "main" ] + branches: [ "dev" ] workflow_dispatch: permissions: @@ -22,6 +22,16 @@ jobs: java-version: '17' distribution: 'temurin' + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Jooq Code Generation run: | ./gradlew --info jooqCodegenAll diff --git a/.gitignore b/.gitignore index 58041a262..23d73c64e 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ api-repo/**/*.sql batch/**/*.sql email/**/*.sql storage/**/*.sql + +# DB explain result files +**/resources/explain/ 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 952aa1b8d..8ec2dcef4 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 @@ -35,7 +35,8 @@ class ArticleMainCardDao( ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "name" ).`as`(ArticleMainCardRecord::writerName.name), - jsonGetAttribute(ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerImgUrl.name), + jsonGetAttribute(ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "url").`as`(ArticleMainCardRecord::writerUrl.name), + jsonGetAttribute(ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, "imageUrl").`as`(ArticleMainCardRecord::writerImgUrl.name), ARTICLE_MAIN_CARD.WORKBOOKS.`as`(ArticleMainCardRecord::workbooks.name) ).from(ARTICLE_MAIN_CARD) .where(ARTICLE_MAIN_CARD.ID.`in`(articleIds)) @@ -70,7 +71,8 @@ class ArticleMainCardDao( commonJsonMapper.toJsonStr( mapOf( "name" to command.writerName, - "url" to command.writerImgUrl + "url" to command.writerUrl, + "imageUrl" to command.writerImgUrl ) ) ) 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 index 0392e27c1..f4658987d 100644 --- 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 @@ -12,5 +12,6 @@ data class ArticleMainCardExcludeWorkbookCommand( val writerId: Long, val writerEmail: String, val writerName: String, + val writerUrl: URL, val writerImgUrl: URL, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleMainCardRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleMainCardRecord.kt index 3833a6634..a3719f451 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleMainCardRecord.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/record/ArticleMainCardRecord.kt @@ -12,6 +12,7 @@ data class ArticleMainCardRecord( val writerId: Long, val writerEmail: String, val writerName: String, + val writerUrl: URL, val writerImgUrl: URL, val workbooks: List = emptyList(), ) { 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 7c4896b53..68acf9d9d 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 @@ -25,6 +25,7 @@ class ArticleMainCardMapper( 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), + writerUrl = record.get(ArticleMainCardRecord::writerUrl.name, URL::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)) { 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 f19a5f8b0..3a0934c73 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.MemberIdAndNameRecord +import com.few.api.repo.dao.member.record.MemberRecord 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.jsonGetAttribute import org.jooq.impl.DSL.jsonGetAttributeAsText import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Repository @@ -42,7 +43,8 @@ class MemberDao( fun selectWriterQuery(query: SelectWriterQuery) = dslContext.select( Member.MEMBER.ID.`as`(WriterRecord::writerId.name), DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(WriterRecord::name.name), - DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name) + DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name), + DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "imageUrl").`as`(WriterRecord::imageUrl.name) ) .from(Member.MEMBER) .where(Member.MEMBER.ID.eq(query.writerId)) @@ -73,7 +75,8 @@ class MemberDao( Member.MEMBER.ID.`as`(WriterRecord::writerId.name), DSL.jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name") .`as`(WriterRecord::name.name), - DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name) + DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(WriterRecord::url.name), + DSL.jsonGetAttribute(Member.MEMBER.DESCRIPTION, "imageUrl").`as`(WriterRecord::imageUrl.name) ) .from(Member.MEMBER) .where(Member.MEMBER.ID.`in`(notCachedIds)) @@ -125,14 +128,19 @@ class MemberDao( .where(Member.MEMBER.TYPE_CD.eq(MemberType.WRITER.code)) .and(Member.MEMBER.DELETED_AT.isNull) - fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberIdAndNameRecord? { + fun selectMemberByEmail(query: SelectMemberByEmailQuery): MemberRecord? { return selectMemberByEmailQuery(query) - .fetchOneInto(MemberIdAndNameRecord::class.java) + .fetchOneInto(MemberRecord::class.java) } fun selectMemberByEmailQuery(query: SelectMemberByEmailQuery) = dslContext.select( - Member.MEMBER.ID.`as`(MemberIdAndNameRecord::memberId.name), - jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(MemberIdAndNameRecord::writerName.name) // writer only(nullable) + Member.MEMBER.ID.`as`(MemberRecord::memberId.name), + // writer only(nullable) + jsonGetAttributeAsText(Member.MEMBER.DESCRIPTION, "name").`as`(MemberRecord::writerName.name), + // writer only(nullable) + jsonGetAttribute(Member.MEMBER.DESCRIPTION, "url").`as`(MemberRecord::url.name), + // writer only(nullable) + jsonGetAttribute(Member.MEMBER.DESCRIPTION, "imageUrl").`as`(MemberRecord::imageUrl.name) ) .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/MemberIdAndNameRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdAndNameRecord.kt deleted file mode 100644 index f2267e120..000000000 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberIdAndNameRecord.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.few.api.repo.dao.member.record - -data class MemberIdAndNameRecord( - val memberId: Long, - val writerName: String?, -) \ No newline at end of file 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/MemberRecord.kt new file mode 100644 index 000000000..f4f21c01c --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/MemberRecord.kt @@ -0,0 +1,10 @@ +package com.few.api.repo.dao.member.record + +import java.net.URL + +data class MemberRecord( + val memberId: Long, + val writerName: String?, // writer only + val imageUrl: URL?, // writer only + val url: URL?, // writer only +) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/WriterRecord.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/WriterRecord.kt index 9b551c421..ff9e7413e 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/WriterRecord.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/record/WriterRecord.kt @@ -6,4 +6,5 @@ data class WriterRecord( val writerId: Long, val name: String, val url: URL, + val imageUrl: URL, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescription.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescription.kt index 7873d12ab..a59a2c994 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescription.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/member/support/WriterDescription.kt @@ -5,4 +5,5 @@ import java.net.URL data class WriterDescription( val name: String, val url: URL, + val imageUrl: URL, ) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt index 636dea158..729a5d156 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt @@ -7,7 +7,7 @@ import com.few.api.repo.dao.subscription.query.CountAllWorkbooksSubscription import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery import com.few.api.repo.dao.subscription.record.WorkbookSubscriptionStatus import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery import com.few.api.repo.dao.subscription.record.CountAllSubscriptionStatusRecord import com.few.api.repo.dao.subscription.record.MemberWorkbookSubscriptionStatusRecord import jooq.jooq_dsl.Tables.MAPPING_WORKBOOK_ARTICLE @@ -69,12 +69,12 @@ class SubscriptionDao( .orderBy(SUBSCRIPTION.CREATED_AT.desc()) .limit(1) - fun selectAllWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery): List { + fun selectAllWorkbookSubscriptionStatus(query: SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery): List { return selectAllWorkbookSubscriptionStatusQuery(query) .fetchInto(MemberWorkbookSubscriptionStatusRecord::class.java) } - fun selectAllWorkbookSubscriptionStatusQuery(query: SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery) = + fun selectAllWorkbookSubscriptionStatusQuery(query: SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery) = dslContext.select( SUBSCRIPTION.TARGET_WORKBOOK_ID.`as`(MemberWorkbookSubscriptionStatusRecord::workbookId.name), SUBSCRIPTION.DELETED_AT.isNull.`as`(MemberWorkbookSubscriptionStatusRecord::isActiveSub.name), @@ -82,10 +82,11 @@ class SubscriptionDao( DSL.max(MAPPING_WORKBOOK_ARTICLE.DAY_COL).`as`(MemberWorkbookSubscriptionStatusRecord::totalDay.name) ) .from(SUBSCRIPTION) - .leftJoin(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) + .join(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) .on(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID)) .where(SUBSCRIPTION.MEMBER_ID.eq(query.memberId)) .and(SUBSCRIPTION.TARGET_MEMBER_ID.isNull) + .and(SUBSCRIPTION.UNSUBS_OPINION.`in`(query.unsubOpinion, query.activeSubscriptionUnsubOpinion)) .groupBy(SUBSCRIPTION.TARGET_WORKBOOK_ID, SUBSCRIPTION.DELETED_AT) .query diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery.kt deleted file mode 100644 index 063f23d69..000000000 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.few.api.repo.dao.subscription.query - -/** - * DeleteAt을 고려하지 않고 멤버의 모든 워크북 구독 상태를 조회하는 쿼리 - */ -data class SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery( - val memberId: Long, -) \ No newline at end of file diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery.kt new file mode 100644 index 000000000..eee092aca --- /dev/null +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/query/SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery.kt @@ -0,0 +1,11 @@ +package com.few.api.repo.dao.subscription.query + +/** + * UnsubOpinion 조건을 만족하고 + * DeleteAt을 고려하지 않고 멤버의 모든 워크북 구독 상태를 조회하는 쿼리 + */ +data class SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery( + val memberId: Long, + val unsubOpinion: String = "receive.all", + val activeSubscriptionUnsubOpinion: String = "", +) \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/MemberDaoTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/member/MemberDaoTest.kt index 64c0fea12..042af219a 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/MemberDaoTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/member/MemberDaoTest.kt @@ -43,7 +43,11 @@ class MemberDaoTest : JooqTestSpec() { .execute() val writerDescription = writerDescriptionJsonMapper.toJson( - WriterDescription("few2", URL("http://localhost:8080/writers/url2")) + WriterDescription( + "few2", + URL("http://localhost:8080/writers/url2"), + URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") + ) ) dslContext.insertInto(Member.MEMBER) @@ -100,7 +104,11 @@ class MemberDaoTest : JooqTestSpec() { fun setMoreWriters(count: Int) { for (i in 3 until 3 + count) { val writerDescription = writerDescriptionJsonMapper.toJson( - WriterDescription("few$i", URL("http://localhost:8080/writers/url$i")) + WriterDescription( + "few$i", + URL("http://localhost:8080/writers/url$i"), + URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") + ) ) dslContext.insertInto(Member.MEMBER) .set(Member.MEMBER.ID, i.toLong()) diff --git a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/CommonJsonMapperTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/CommonJsonMapperTest.kt index 35e030841..e5a0ac059 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/CommonJsonMapperTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/dao/member/support/CommonJsonMapperTest.kt @@ -15,7 +15,8 @@ class CommonJsonMapperTest { // Given val writerDescription = WriterDescription( name = "writer", - url = URL("http://localhost:8080/writers/url") + url = URL("http://localhost:8080/writers/url"), + imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") ) // When @@ -34,7 +35,8 @@ class CommonJsonMapperTest { val json = """ { "name": "writer", - "url": "http://localhost:8080/writers/url" + "url": "http://localhost:8080/writers/url", + "imageUrl": "https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742" } """.trimIndent() diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt index 433e5dd4e..1bed29ec0 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt @@ -4,6 +4,7 @@ import com.few.api.repo.dao.member.MemberDao import com.few.api.repo.dao.member.command.InsertMemberCommand import com.few.api.repo.dao.member.command.UpdateDeletedMemberTypeCommand import com.few.api.repo.dao.member.command.UpdateMemberTypeCommand +import com.few.api.repo.dao.member.query.BrowseWorkbookWritersQuery 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.support.WriterDescription @@ -47,7 +48,11 @@ class MemberDaoExplainGenerateTest : JooqTestSpec() { .execute() val writerDescription = writerDescriptionJsonMapper.toJson( - WriterDescription("few2", URL("http://localhost:8080/writers/url2")) + WriterDescription( + "few2", + URL("http://localhost:8080/writers/url2"), + URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") + ) ) dslContext.insertInto(Member.MEMBER) @@ -88,6 +93,17 @@ class MemberDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "selectWritersQueryExplain") } + @Test + fun selectWritersQueryExplainByWorkbookIds() { + val query = BrowseWorkbookWritersQuery(listOf(2L, 3L)).let { + memberDao.selectWritersQuery(it) + } + + val explain = dslContext.explain(query).toString() + + ResultGenerator.execute(query, explain, "selectWritersQueryExplainByWorkbookIds") + } + @Test fun selectMemberByEmailQueryExplain() { val query = SelectMemberByEmailQuery("member@gmail.com").let { diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt index 76dd2a8df..06a08e8ae 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt @@ -5,7 +5,7 @@ import com.few.api.repo.dao.subscription.command.InsertWorkbookSubscriptionComma import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInAllSubscriptionCommand import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery import com.few.api.repo.explain.InsertUpdateExplainGenerator import com.few.api.repo.explain.ResultGenerator import com.few.api.repo.jooq.JooqTestSpec @@ -62,7 +62,11 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { @Test fun selectAllTopWorkbookSubscriptionStatusQueryExplain() { - val query = SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery(memberId = 1L).let { + val query = SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery( + memberId = 1L, + unsubOpinion = "receive.all", + activeSubscriptionUnsubOpinion = "" + ).let { subscriptionDao.selectAllWorkbookSubscriptionStatusQuery(it) } diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 49cb5faa2..88ab9c3e3 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -19,6 +19,11 @@ dependencies { implementation("io.jsonwebtoken:jjwt-impl:${DependencyVersion.JWT}") implementation("io.jsonwebtoken:jjwt-jackson:${DependencyVersion.JWT}") + /** scrimage */ + implementation("com.sksamuel.scrimage:scrimage-core:${DependencyVersion.SCRIMAGE}") + /** for convert to webp */ + implementation("com.sksamuel.scrimage:scrimage-webp:${DependencyVersion.SCRIMAGE}") + /** swagger & restdocs */ implementation("org.springdoc:springdoc-openapi-ui:${DependencyVersion.SPRINGDOC}") implementation("org.springframework.restdocs:spring-restdocs-webtestclient") @@ -28,6 +33,7 @@ dependencies { /** test container */ implementation(platform("org.testcontainers:testcontainers-bom:${DependencyVersion.TEST_CONTAINER}")) + testImplementation("org.springframework.security:spring-security-test") testImplementation("org.testcontainers:mysql") } diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt index 9d7643e40..30d6c28a0 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/service/ArticleMainCardService.kt @@ -28,6 +28,7 @@ class ArticleMainCardService( writerId = inDto.writerId, writerEmail = inDto.writerEmail, writerName = inDto.writerName, + writerUrl = inDto.writerUrl, writerImgUrl = inDto.writerImgUrl ) ) 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 index d013a7484..76ce6870e 100644 --- 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 @@ -12,5 +12,6 @@ data class InitializeArticleMainCardInDto( val writerId: Long, val writerEmail: String, val writerName: String, + val writerUrl: URL, 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 910be40b7..5218dfd03 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 @@ -27,7 +27,6 @@ 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.* @@ -46,7 +45,7 @@ class AddArticleUseCase( @Transactional fun execute(useCaseIn: AddArticleUseCaseIn): AddArticleUseCaseOut { /** select writerId */ - val writerIdRecord = SelectMemberByEmailQuery(useCaseIn.writerEmail).let { + val writerRecord = SelectMemberByEmailQuery(useCaseIn.writerEmail).let { memberDao.selectMemberByEmail(it) } ?: throw NotFoundException("member.notfound.id") @@ -101,7 +100,7 @@ class AddArticleUseCase( /** insert article */ val articleMstId = InsertFullArticleRecordCommand( - writerId = writerIdRecord.memberId, + writerId = writerRecord.memberId, mainImageURL = useCaseIn.articleImageUrl, title = useCaseIn.title, category = category.code, @@ -141,10 +140,11 @@ class AddArticleUseCase( mainImageUrl = useCaseIn.articleImageUrl, categoryCd = category.code, createdAt = LocalDateTime.now(), // TODO: DB insert 시점으로 변경 - writerId = writerIdRecord.memberId, + writerId = writerRecord.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: 작가 이미지로 변환 + writerName = writerRecord.writerName ?: throw NotFoundException("article.writer.name"), + writerUrl = writerRecord.url ?: throw NotFoundException("article.writer.url"), + writerImgUrl = writerRecord.imageUrl ?: throw NotFoundException("article.writer.url") ) ) diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt index ec44fc025..2a066cdf3 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/PutImageUseCase.kt @@ -10,6 +10,8 @@ import com.few.api.exception.common.InsertException import com.few.api.repo.dao.image.ImageDao import com.few.api.repo.dao.image.command.InsertImageIfoCommand import com.few.storage.image.service.PutImageService +import com.sksamuel.scrimage.ImmutableImage +import com.sksamuel.scrimage.webp.WebpWriter import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import java.io.File @@ -26,14 +28,17 @@ class PutImageUseCase( val imageSource = useCaseIn.source val suffix = imageSource.originalFilename?.substringAfterLast(".") ?: "jpg" - val image = runCatching { + val imageName = ObjectPathGenerator.imagePath(suffix) + val originImage = runCatching { File.createTempFile("temp", ".$suffix") }.onSuccess { imageSource.transferTo(it) }.getOrThrow() - val imageName = ObjectPathGenerator.imagePath(suffix) - val url = putImageService.execute(imageName, image)?.let { res -> + val webpImage = ImmutableImage.loader().fromFile(originImage) + .output(WebpWriter.DEFAULT, File.createTempFile("temp", ".webp")) + + val url = putImageService.execute(imageName, originImage)?.let { res -> val source = res.`object` GetUrlInDto(source).let { query -> getUrlService.execute(query) @@ -45,6 +50,20 @@ class PutImageUseCase( } } ?: throw ExternalIntegrationException("external.presignedfail.image") - return PutImageUseCaseOut(url!!) + val webpUrl = + putImageService.execute(imageName.replaceAfterLast(".", "webp"), webpImage)?.let { res -> + val source = res.`object` + GetUrlInDto(source).let { query -> + getUrlService.execute(query) + }.let { dto -> + InsertImageIfoCommand(source, dto.url).let { command -> + imageDao.insertImageIfo(command) ?: throw InsertException("image.insertfail.record") + } + return@let dto.url + } + } ?: throw ExternalIntegrationException("external.presignedfail.image") + + // todo fix if webp is default + return PutImageUseCaseOut(url, listOf(suffix, "webp")) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/dto/PutImageUseCaseOut.kt b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/dto/PutImageUseCaseOut.kt index 43c18116c..1842c4d8c 100644 --- a/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/dto/PutImageUseCaseOut.kt +++ b/api/src/main/kotlin/com/few/api/domain/admin/document/usecase/dto/PutImageUseCaseOut.kt @@ -4,4 +4,5 @@ import java.net.URL data class PutImageUseCaseOut( val url: URL, + val supportSuffix: List, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt b/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt index 79994cc08..ff010ad2c 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/service/ReadArticleWriterRecordService.kt @@ -18,7 +18,8 @@ class ReadArticleWriterRecordService( ReadWriterOutDto( writerId = it.writerId, name = it.name, - url = it.url + url = it.url, + imageUrl = it.imageUrl ) } } diff --git a/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadWriterOutDto.kt b/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadWriterOutDto.kt index 418671e8b..6f35ef0d7 100644 --- a/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadWriterOutDto.kt +++ b/api/src/main/kotlin/com/few/api/domain/article/service/dto/ReadWriterOutDto.kt @@ -6,4 +6,5 @@ data class ReadWriterOutDto( val writerId: Long, val name: String, val url: URL, + val imageUrl: URL, ) \ 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 e536dcbae..2623ff980 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 @@ -59,7 +59,8 @@ class ReadArticleUseCase( writer = WriterDetail( id = writerRecord.writerId, name = writerRecord.name, - url = writerRecord.url + url = writerRecord.url, + imageUrl = writerRecord.imageUrl ), mainImageUrl = articleRecord.mainImageURL, title = articleRecord.title, 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 c6c750310..490221779 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 @@ -81,7 +81,8 @@ class ReadArticlesUseCase( writer = WriterDetail( id = a.writerId, name = a.writerName, - url = a.writerImgUrl + imageUrl = a.writerImgUrl, + url = a.writerUrl ), mainImageUrl = a.mainImageUrl, title = a.articleTitle, 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 a5ac939cd..f0aae15e1 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 @@ -20,6 +20,7 @@ data class WriterDetail( val id: Long, val name: String, val url: URL, + val imageUrl: URL, ) data class WorkbookDetail( diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt index 3b722afae..0c20421b2 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCase.kt @@ -23,6 +23,13 @@ class SaveMemberUseCase( private val sendAuthEmailService: SendAuthEmailService, private val idEncryption: IdEncryption, ) { + companion object { + private const val AUTH_HEAD_COMMENT = "few 로그인 링크입니다." + private const val AUTH_SUB_COMMENT = "로그인하시려면 아래 버튼을 눌러주세요!" + private const val SIGNUP_HEAD_COMMENT = "few에 가입해주셔서 감사합니다." + private const val SIGNUP_SUB_COMMENT = "가입하신 이메일 주소를 확인해주세요." + } + @Transactional fun execute(useCaseIn: SaveMemberUseCaseIn): SaveMemberUseCaseOut { /** email을 통해 가입 이력이 있는지 확인 */ @@ -32,8 +39,15 @@ class SaveMemberUseCase( memberDao.selectMemberByEmail(it) } + var headComment = AUTH_HEAD_COMMENT + var subComment = AUTH_SUB_COMMENT + var email = "" + /** 가입 이력이 없다면 회원 가입 처리 */ val token = if (Objects.isNull(isSignUpBeforeMember)) { + headComment = SIGNUP_HEAD_COMMENT + subComment = SIGNUP_SUB_COMMENT + email = useCaseIn.email InsertMemberCommand( email = useCaseIn.email, memberType = MemberType.PREAUTH @@ -64,8 +78,10 @@ class SaveMemberUseCase( subject = "[FEW] 인증 이메일 주소를 확인해주세요.", template = "auth", content = Content( - email = useCaseIn.email, - confirmLink = URL("https://www.fewletter.com/auth/validation/complete?token=$token") + headComment = headComment, + subComment = subComment, + email = email, + confirmLink = URL("https://www.fewletter.com/auth/validation/complete?auth_token=$token") ) ).let { sendAuthEmailService.send(it) diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt index a9c203433..61bce9a10 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt @@ -8,7 +8,7 @@ import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCa import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookDetail import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.query.CountAllWorkbooksSubscription -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery import com.few.api.web.support.WorkBookStatus import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -22,7 +22,7 @@ class BrowseSubscribeWorkbooksUseCase( @Transactional fun execute(useCaseIn: BrowseSubscribeWorkbooksUseCaseIn): BrowseSubscribeWorkbooksUseCaseOut { val subscriptionRecords = - SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery(useCaseIn.memberId).let { + SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery(useCaseIn.memberId).let { subscriptionDao.selectAllWorkbookSubscriptionStatus(it) } 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 88f8e307d..76bdb0425 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt @@ -1,9 +1,6 @@ package com.few.api.domain.subscription.usecase import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent -import com.few.api.domain.subscription.service.MemberService -import com.few.api.domain.subscription.service.dto.InsertMemberInDto -import com.few.api.domain.subscription.service.dto.ReadMemberIdInDto import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.InsertWorkbookSubscriptionCommand import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery @@ -11,7 +8,6 @@ import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookUseCaseIn import com.few.api.exception.common.NotFoundException import com.few.api.exception.subscribe.SubscribeIllegalArgumentException import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery -import com.few.data.common.code.MemberType import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -19,7 +15,6 @@ import org.springframework.transaction.annotation.Transactional @Component class SubscribeWorkbookUseCase( private val subscriptionDao: SubscriptionDao, - private val memberService: MemberService, private val applicationEventPublisher: ApplicationEventPublisher, ) { @@ -27,11 +22,8 @@ class SubscribeWorkbookUseCase( fun execute(useCaseIn: SubscribeWorkbookUseCaseIn) { // TODO: request sending email - val memberId = memberService.readMemberId(ReadMemberIdInDto(useCaseIn.email))?.memberId ?: memberService.insertMember( - InsertMemberInDto(email = useCaseIn.email, memberType = MemberType.NORMAL) - ).memberId - val subTargetWorkbookId = useCaseIn.workbookId + val memberId = useCaseIn.memberId val command = InsertWorkbookSubscriptionCommand( memberId = memberId, workbookId = subTargetWorkbookId 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 0b53830ce..1887d9453 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 @@ -1,9 +1,7 @@ package com.few.api.domain.subscription.usecase import com.few.api.domain.subscription.service.MemberService -import com.few.api.domain.subscription.service.dto.ReadMemberIdInDto import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn -import com.few.api.exception.common.NotFoundException import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInAllSubscriptionCommand import org.springframework.stereotype.Component @@ -18,12 +16,16 @@ class UnsubscribeAllUseCase( @Transactional fun execute(useCaseIn: UnsubscribeAllUseCaseIn) { // TODO: request sending email - - val memberId = - memberService.readMemberId(ReadMemberIdInDto(useCaseIn.email))?.memberId ?: throw NotFoundException("member.notfound.email") + var opinion = useCaseIn.opinion + if (useCaseIn.opinion == "") { + opinion = "cancel" + } subscriptionDao.updateDeletedAtInAllSubscription( - UpdateDeletedAtInAllSubscriptionCommand(memberId = memberId, opinion = useCaseIn.opinion) + UpdateDeletedAtInAllSubscriptionCommand( + memberId = useCaseIn.memberId, + opinion = opinion + ) ) } } \ No newline at end of file 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 7f5c31bda..ba5afb038 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 @@ -1,11 +1,9 @@ package com.few.api.domain.subscription.usecase import com.few.api.domain.subscription.service.MemberService -import com.few.api.domain.subscription.service.dto.ReadMemberIdInDto import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInWorkbookSubscriptionCommand import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn -import com.few.api.exception.common.NotFoundException import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -18,15 +16,16 @@ class UnsubscribeWorkbookUseCase( @Transactional fun execute(useCaseIn: UnsubscribeWorkbookUseCaseIn) { // TODO: request sending email - - val memberId = - memberService.readMemberId(ReadMemberIdInDto(useCaseIn.email))?.memberId ?: throw NotFoundException("member.notfound.email") + var opinion = useCaseIn.opinion + if (useCaseIn.opinion == "") { + opinion = "cancel" + } subscriptionDao.updateDeletedAtInWorkbookSubscription( UpdateDeletedAtInWorkbookSubscriptionCommand( - memberId = memberId, + memberId = useCaseIn.memberId, workbookId = useCaseIn.workbookId, - opinion = useCaseIn.opinion + opinion = opinion ) ) } diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/SubscribeWorkbookUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/SubscribeWorkbookUseCaseIn.kt index 775639cf8..64175496e 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/SubscribeWorkbookUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/SubscribeWorkbookUseCaseIn.kt @@ -2,5 +2,5 @@ package com.few.api.domain.subscription.usecase.dto data class SubscribeWorkbookUseCaseIn( val workbookId: Long, - val email: String, + val memberId: Long, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeAllUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeAllUseCaseIn.kt index c6e2d4b6a..35c4b5ac1 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeAllUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeAllUseCaseIn.kt @@ -2,5 +2,5 @@ package com.few.api.domain.subscription.usecase.dto data class UnsubscribeAllUseCaseIn( val opinion: String, - val email: String, + val memberId: Long, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeWorkbookUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeWorkbookUseCaseIn.kt index afebaaad1..2e8203ceb 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeWorkbookUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/dto/UnsubscribeWorkbookUseCaseIn.kt @@ -2,6 +2,6 @@ package com.few.api.domain.subscription.usecase.dto data class UnsubscribeWorkbookUseCaseIn( val workbookId: Long, - val email: String, + val memberId: Long, val opinion: String, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/article/dto/ReadWorkBookArticleUseCaseIn.kt b/api/src/main/kotlin/com/few/api/domain/workbook/article/dto/ReadWorkBookArticleUseCaseIn.kt index 3ff7f88f1..108377a9c 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/article/dto/ReadWorkBookArticleUseCaseIn.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/article/dto/ReadWorkBookArticleUseCaseIn.kt @@ -3,4 +3,5 @@ package com.few.api.domain.workbook.article.dto data class ReadWorkBookArticleUseCaseIn( val workbookId: Long, val articleId: Long, + val memberId: Long, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/article/usecase/ReadWorkBookArticleUseCase.kt index 12f09041f..8734a1401 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 @@ -1,5 +1,7 @@ package com.few.api.domain.workbook.article.usecase +import com.few.api.domain.article.handler.ArticleViewCountHandler +import com.few.api.domain.article.handler.ArticleViewHisAsyncHandler import com.few.api.domain.article.service.BrowseArticleProblemsService import com.few.api.domain.article.service.ReadArticleWriterRecordService import com.few.api.domain.article.service.dto.BrowseArticleProblemIdsInDto @@ -19,6 +21,8 @@ class ReadWorkBookArticleUseCase( private val articleDao: ArticleDao, private val readArticleWriterRecordService: ReadArticleWriterRecordService, private val browseArticleProblemsService: BrowseArticleProblemsService, + private val articleViewHisAsyncHandler: ArticleViewHisAsyncHandler, + private val articleViewCountHandler: ArticleViewCountHandler, ) { @Transactional(readOnly = true) fun execute(useCaseIn: ReadWorkBookArticleUseCaseIn): ReadWorkBookArticleOut { @@ -39,6 +43,23 @@ class ReadWorkBookArticleUseCase( browseArticleProblemsService.execute(query) } + /** + * @see com.few.api.domain.article.usecase.ReadArticleUseCase + */ + // ARTICLE VIEW HIS에 저장하기 전에 먼저 VIEW COUNT 조회하는 순서 변경 금지 + val views = articleViewCountHandler.browseArticleViewCount(useCaseIn.articleId) + articleViewHisAsyncHandler.addArticleViewHis( + useCaseIn.articleId, + useCaseIn.memberId, + CategoryType.fromCode(articleRecord.category) ?: throw NotFoundException("article.invalid.category") + ) + + /** + * NOTE: The articleViewHisAsyncHandler creates a new transaction that is separate from the current context. + * So this section, the logic after the articleViewHisAsyncHandler call, + * is where the mismatch between the two transactions can occur if an exception is thrown. + */ + return ReadWorkBookArticleOut( id = articleRecord.articleId, writer = WriterDetail( diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt index ac2cb4377..c23a4e021 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/service/WorkbookSubscribeService.kt @@ -3,7 +3,7 @@ package com.few.api.domain.workbook.service import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksInDto import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksOutDto import com.few.api.repo.dao.subscription.SubscriptionDao -import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery +import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery import org.springframework.stereotype.Service @Service @@ -12,7 +12,7 @@ class WorkbookSubscribeService( ) { fun browseMemberSubscribeWorkbooks(dto: BrowseMemberSubscribeWorkbooksInDto): List { - return SelectAllMemberWorkbookSubscriptionStatusNotConsiderDeletedAtQuery(dto.memberId).let { it -> + return SelectAllMemberWorkbookSubscriptionStatusUnsubOpinionConditionAndNotConsiderDeletedAQuery(dto.memberId).let { it -> subscriptionDao.selectAllWorkbookSubscriptionStatus(it).map { BrowseMemberSubscribeWorkbooksOutDto( workbookId = it.workbookId, diff --git a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt index 7d56e72e6..69c9664e1 100644 --- a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt +++ b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt @@ -31,8 +31,9 @@ class TokenUserDetailsService( val id = claims.get( MEMBER_ID_CLAIM_KEY, - Long::class.java - ) + Integer::class.java + ).toLong() + val roles = claims.get( MEMBER_ROLE_CLAIM_KEY, String::class.java diff --git a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt index 78c88e2f1..0b0dfccc9 100644 --- a/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt +++ b/api/src/main/kotlin/com/few/api/security/config/WebSecurityConfig.kt @@ -132,7 +132,32 @@ class WebSecurityConfig( AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()), AntPathRequestMatcher("/openapi3.yaml", HttpMethod.GET.name()), AntPathRequestMatcher("/reports/**", HttpMethod.GET.name()), - AntPathRequestMatcher("/api/v1/**") // todo fix + + /** 인증/비인증 모두 허용 */ + AntPathRequestMatcher( + "/api/v1/subscriptions/workbooks/main", + HttpMethod.GET.name() + ), + AntPathRequestMatcher("/api/v1/workbooks", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/articles/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*/articles/*", HttpMethod.GET.name()), + + /** 어드민 */ + AntPathRequestMatcher("/api/v1/admin/**", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/logs", HttpMethod.POST.name()), + AntPathRequestMatcher("/batch/**"), + + /** 인증 불필요 */ + AntPathRequestMatcher("/api/v1/members", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/members/token", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/articles", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/articles/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*/articles/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/problems/**", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/problems/*", HttpMethod.POST.name()) ) } } @@ -151,8 +176,32 @@ class WebSecurityConfig( AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()), AntPathRequestMatcher("/openapi3.yaml", HttpMethod.GET.name()), AntPathRequestMatcher("/reports/**", HttpMethod.GET.name()), - AntPathRequestMatcher("/api/v1/**"), // todo fix - AntPathRequestMatcher("/batch/**") // todo fix + + /** 인증/비인증 모두 허용 */ + AntPathRequestMatcher( + "/api/v1/subscriptions/workbooks/main", + HttpMethod.GET.name() + ), + AntPathRequestMatcher("/api/v1/workbooks", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/articles/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*/articles/*", HttpMethod.GET.name()), + + /** 어드민 */ + AntPathRequestMatcher("/api/v1/admin/**", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/logs", HttpMethod.POST.name()), + AntPathRequestMatcher("/batch/**"), + + /** 인증 불필요 */ + AntPathRequestMatcher("/api/v1/members", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/members/token", HttpMethod.POST.name()), + AntPathRequestMatcher("/api/v1/articles", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/articles/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/categories", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/workbooks/*/articles/*", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/problems/**", HttpMethod.GET.name()), + AntPathRequestMatcher("/api/v1/problems/*", HttpMethod.POST.name()) ) } } diff --git a/api/src/main/kotlin/com/few/api/security/filter/exception/TokenInvalidExceptionHandlerFilter.kt b/api/src/main/kotlin/com/few/api/security/filter/exception/TokenInvalidExceptionHandlerFilter.kt index 02c64d44c..8a18baa71 100644 --- a/api/src/main/kotlin/com/few/api/security/filter/exception/TokenInvalidExceptionHandlerFilter.kt +++ b/api/src/main/kotlin/com/few/api/security/filter/exception/TokenInvalidExceptionHandlerFilter.kt @@ -38,7 +38,7 @@ class TokenInvalidExceptionHandlerFilter : OncePerRequestFilter() { private const val message = "인증이 필요해요." } override fun toString(): String { - return "{ message: \"$message\" }" + return "{ \"message\": \"$message\" }" } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/security/token/TokenResolver.kt b/api/src/main/kotlin/com/few/api/security/token/TokenResolver.kt index 98df1792e..a74a6ddbc 100644 --- a/api/src/main/kotlin/com/few/api/security/token/TokenResolver.kt +++ b/api/src/main/kotlin/com/few/api/security/token/TokenResolver.kt @@ -40,7 +40,8 @@ class TokenResolver( .build() .parseClaimsJws(token) .body - .get(MEMBER_ID_CLAIM_KEY, Long::class.java) + .get(MEMBER_ID_CLAIM_KEY, Integer::class.java) + .toLong() } catch (e: Exception) { log.warn { "${"Failed to get memberId. token: {}"} $token" } return null 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 01316b3ef..add687c65 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 @@ -113,7 +113,7 @@ class AdminController( putImageUseCase.execute(useCaseIn) } - return ImageSourceResponse(useCaseOut.url).let { + return ImageSourceResponse(useCaseOut.url, useCaseOut.supportSuffix).let { ApiResponseGenerator.success(it, HttpStatus.OK) } } diff --git a/api/src/main/kotlin/com/few/api/web/controller/admin/response/ImageSourceResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/admin/response/ImageSourceResponse.kt index 62caa77c6..8ada1c269 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/admin/response/ImageSourceResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/admin/response/ImageSourceResponse.kt @@ -4,4 +4,5 @@ import java.net.URL data class ImageSourceResponse( val url: URL, + val supportSuffix: List, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt index 29aebb01a..e9e32e7a1 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt @@ -1,13 +1,19 @@ package com.few.api.web.controller.article -import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.domain.article.usecase.ReadArticleUseCase import com.few.api.domain.article.usecase.ReadArticlesUseCase +import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn -import com.few.api.web.controller.article.response.* +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver +import com.few.api.web.controller.article.response.ReadArticleResponse +import com.few.api.web.controller.article.response.ReadArticlesResponse +import com.few.api.web.controller.article.response.WorkbookInfo +import com.few.api.web.controller.article.response.WriterInfo import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator import com.few.data.common.code.CategoryType +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -20,18 +26,27 @@ import org.springframework.web.bind.annotation.* class ArticleController( private val readArticleUseCase: ReadArticleUseCase, private val readArticlesUseCase: ReadArticlesUseCase, + private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readArticle( + servletRequest: HttpServletRequest, @PathVariable(value = "articleId") @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { + val authorization: String? = servletRequest.getHeader("Authorization") + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) + } ?: 0L + val useCaseOut = ReadArticleUseCaseIn( articleId = articleId, - memberId = 0L - ).let { useCaseIn: ReadArticleUseCaseIn -> //TODO: membberId검토 + memberId = memberId + ).let { useCaseIn: ReadArticleUseCaseIn -> readArticleUseCase.execute(useCaseIn) } @@ -39,9 +54,10 @@ class ArticleController( id = useCaseOut.id, title = useCaseOut.title, writer = WriterInfo( - useCaseOut.writer.id, - useCaseOut.writer.name, - useCaseOut.writer.url + id = useCaseOut.writer.id, + name = useCaseOut.writer.name, + url = useCaseOut.writer.url, + imageUrl = useCaseOut.writer.imageUrl ), mainImageUrl = useCaseOut.mainImageUrl, content = useCaseOut.content, @@ -72,9 +88,10 @@ class ArticleController( id = a.id, title = a.title, writer = WriterInfo( - a.writer.id, - a.writer.name, - a.writer.url + id = a.writer.id, + name = a.writer.name, + url = a.writer.url, + imageUrl = a.writer.imageUrl ), mainImageUrl = a.mainImageUrl, content = a.content, 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 d587c3ce6..fa609cfa0 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 @@ -22,6 +22,7 @@ data class WriterInfo( val id: Long, val name: String, val url: URL, + val imageUrl: URL, ) data class WorkbookInfo( diff --git a/api/src/main/kotlin/com/few/api/web/controller/member/MemberController.kt b/api/src/main/kotlin/com/few/api/web/controller/member/MemberController.kt index 1886e66e5..f1677d3cf 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/member/MemberController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/member/MemberController.kt @@ -6,6 +6,7 @@ import com.few.api.domain.member.usecase.TokenUseCase import com.few.api.domain.member.usecase.dto.DeleteMemberUseCaseIn import com.few.api.domain.member.usecase.dto.SaveMemberUseCaseIn import com.few.api.domain.member.usecase.dto.TokenUseCaseIn +import com.few.api.security.authentication.token.TokenUserDetails import com.few.api.web.controller.member.request.SaveMemberRequest import com.few.api.web.controller.member.request.TokenRequest import com.few.api.web.controller.member.response.SaveMemberResponse @@ -14,6 +15,7 @@ import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.PostMapping @@ -47,10 +49,11 @@ class MemberController( } } - // todo add controller test after security is implemented @DeleteMapping() - fun deleteMember(): ApiResponse { - val memberId = 1L // todo fix + fun deleteMember( + @AuthenticationPrincipal userDetails: TokenUserDetails, + ): ApiResponse { + val memberId = userDetails.username.toLong() val useCaseOut = DeleteMemberUseCaseIn( memberId = memberId ).let { diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt index 7aac2e1b1..88bf98f5e 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/SubscriptionController.kt @@ -1,7 +1,6 @@ package com.few.api.web.controller.subscription import com.few.api.domain.subscription.usecase.BrowseSubscribeWorkbooksUseCase -import com.few.api.web.controller.subscription.request.SubscribeWorkbookRequest import com.few.api.web.controller.subscription.request.UnsubscribeWorkbookRequest import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator @@ -14,6 +13,9 @@ import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn +import com.few.api.security.authentication.token.TokenUserDetails +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.subscription.request.UnsubscribeAllRequest import com.few.api.web.controller.subscription.response.MainViewBrowseSubscribeWorkbooksResponse import com.few.api.web.controller.subscription.response.MainViewSubscribeWorkbookInfo @@ -21,10 +23,12 @@ import com.few.api.web.controller.subscription.response.SubscribeWorkbookInfo import com.few.api.web.controller.subscription.response.SubscribeWorkbooksResponse import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookCategory +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.validation.annotation.Validated import org.springframework.web.bind.annotation.* @@ -36,21 +40,21 @@ class SubscriptionController( private val unsubscribeWorkbookUseCase: UnsubscribeWorkbookUseCase, private val unsubscribeAllUseCase: UnsubscribeAllUseCase, private val browseSubscribeWorkbooksUseCase: BrowseSubscribeWorkbooksUseCase, + private val tokenResolver: TokenResolver, // 임시 구현용 private val browseWorkBooksUseCase: BrowseWorkbooksUseCase, ) { - // todo add auth @GetMapping("/subscriptions/workbooks") fun browseSubscribeWorkbooks( + @AuthenticationPrincipal userDetails: TokenUserDetails, @RequestParam( value = "view", required = false ) view: ViewCategory? = ViewCategory.MAIN_CARD, ): ApiResponse> { - // todo fix memberId - val memberId = 1L + val memberId = userDetails.username.toLong() val useCaseOut = BrowseSubscribeWorkbooksUseCaseIn(memberId).let { browseSubscribeWorkbooksUseCase.execute(it) } @@ -72,75 +76,122 @@ class SubscriptionController( } } - // 임시 구현 @GetMapping("/subscriptions/workbooks/main") fun mainViewBrowseSubscribeWorkbooks( + servletRequest: HttpServletRequest, @RequestParam(value = "category", required = false) category: WorkBookCategory?, ): ApiResponse> { - // todo fix memberId - val memberId = 1L - val memberSubscribeWorkbooks = BrowseSubscribeWorkbooksUseCaseIn(memberId).let { - browseSubscribeWorkbooksUseCase.execute(it) + val authorization: String? = servletRequest.getHeader("Authorization") + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) } - val workbooks = - BrowseWorkbooksUseCaseIn( - category ?: WorkBookCategory.All, - ViewCategory.MAIN_CARD, - memberId - ).let { useCaseIn -> - browseWorkBooksUseCase.execute(useCaseIn) + + if (memberId != null) { + val memberSubscribeWorkbooks = BrowseSubscribeWorkbooksUseCaseIn(memberId).let { + browseSubscribeWorkbooksUseCase.execute(it) } + val workbooks = + BrowseWorkbooksUseCaseIn( + category ?: WorkBookCategory.All, + ViewCategory.MAIN_CARD, + memberId + ).let { useCaseIn -> + browseWorkBooksUseCase.execute(useCaseIn) + } - return MainViewBrowseSubscribeWorkbooksResponse( - workbooks = workbooks.workbooks.map { - MainViewSubscribeWorkbookInfo( - id = it.id, - mainImageUrl = it.mainImageUrl, - title = it.title, - description = it.description, - category = it.category, - createdAt = it.createdAt, - writerDetails = it.writerDetails, - subscriptionCount = it.subscriptionCount, - status = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.isActiveSub?.name, - totalDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalDay, - currentDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.currentDay, - rank = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.rank, - totalSubscriber = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalSubscriber, - articleInfo = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.articleInfo - ) + return MainViewBrowseSubscribeWorkbooksResponse( + workbooks = workbooks.workbooks.map { + MainViewSubscribeWorkbookInfo( + id = it.id, + mainImageUrl = it.mainImageUrl, + title = it.title, + description = it.description, + category = it.category, + createdAt = it.createdAt, + writerDetails = it.writerDetails, + subscriptionCount = it.subscriptionCount, + status = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.isActiveSub?.name, + totalDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalDay, + currentDay = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.currentDay, + rank = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.rank, + totalSubscriber = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.totalSubscriber, + articleInfo = memberSubscribeWorkbooks.workbooks.find { subscribe -> subscribe.workbookId == it.id }?.articleInfo + ) + } + ).let { + ApiResponseGenerator.success(it, HttpStatus.OK) + } + } else { + val workbooks = + BrowseWorkbooksUseCaseIn( + category ?: WorkBookCategory.All, + ViewCategory.MAIN_CARD, + memberId + ).let { useCaseIn -> + browseWorkBooksUseCase.execute(useCaseIn) + } + + return MainViewBrowseSubscribeWorkbooksResponse( + workbooks = workbooks.workbooks.map { + MainViewSubscribeWorkbookInfo( + id = it.id, + mainImageUrl = it.mainImageUrl, + title = it.title, + description = it.description, + category = it.category, + createdAt = it.createdAt, + writerDetails = it.writerDetails, + subscriptionCount = it.subscriptionCount, + status = null, + totalDay = null, + currentDay = null, + rank = null, + totalSubscriber = null, + articleInfo = null + ) + } + ).let { + ApiResponseGenerator.success(it, HttpStatus.OK) } - ).let { - ApiResponseGenerator.success(it, HttpStatus.OK) } } + // todo fix email to memberId @PostMapping("/workbooks/{workbookId}/subs") fun subscribeWorkbook( + @AuthenticationPrincipal userDetails: TokenUserDetails, @PathVariable(value = "workbookId") @Min(value = 1, message = "{min.id}") workbookId: Long, - @Valid @RequestBody - body: SubscribeWorkbookRequest, ): ApiResponse { + val memberId = userDetails.username.toLong() subscribeWorkbookUseCase.execute( - SubscribeWorkbookUseCaseIn(workbookId = workbookId, email = body.email) + SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = memberId) ) return ApiResponseGenerator.success(HttpStatus.OK) } + // todo fix email to memberId @PostMapping("/workbooks/{workbookId}/unsubs") fun unsubscribeWorkbook( + @AuthenticationPrincipal userDetails: TokenUserDetails, @PathVariable(value = "workbookId") @Min(value = 1, message = "{min.id}") workbookId: Long, @Valid @RequestBody body: UnsubscribeWorkbookRequest, ): ApiResponse { + val memberId = userDetails.username.toLong() unsubscribeWorkbookUseCase.execute( - UnsubscribeWorkbookUseCaseIn(workbookId = workbookId, email = body.email, opinion = body.opinion) + UnsubscribeWorkbookUseCaseIn( + workbookId = workbookId, + memberId = memberId, + opinion = body.opinion + ) ) return ApiResponseGenerator.success(HttpStatus.OK) @@ -148,11 +199,13 @@ class SubscriptionController( @PostMapping("/subscriptions/unsubs") fun deactivateAllSubscriptions( + @AuthenticationPrincipal userDetails: TokenUserDetails, @Valid @RequestBody body: UnsubscribeAllRequest, ): ApiResponse { + val memberId = userDetails.username.toLong() unsubscribeAllUseCase.execute( - UnsubscribeAllUseCaseIn(email = body.email, opinion = body.opinion) + UnsubscribeAllUseCaseIn(memberId = memberId, opinion = body.opinion) ) return ApiResponseGenerator.success(HttpStatus.OK) diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/SubscribeWorkbookRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/SubscribeWorkbookRequest.kt deleted file mode 100644 index d0f52ffa7..000000000 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/SubscribeWorkbookRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.few.api.web.controller.subscription.request - -import jakarta.validation.constraints.Email -import jakarta.validation.constraints.NotBlank - -data class SubscribeWorkbookRequest( - @field:NotBlank(message = "{email.notblank}") - @field:Email(message = "{email.invalid}") - val email: String, -) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeAllRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeAllRequest.kt index 14e24af35..52716ef45 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeAllRequest.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeAllRequest.kt @@ -1,11 +1,5 @@ package com.few.api.web.controller.subscription.request -import jakarta.validation.constraints.Email -import jakarta.validation.constraints.NotBlank - data class UnsubscribeAllRequest( - @field:NotBlank(message = "{email.notblank}") - @field:Email(message = "{email.invalid}") - val email: String, val opinion: String, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeWorkbookRequest.kt b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeWorkbookRequest.kt index 5c61080a4..9f6bb6462 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeWorkbookRequest.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/subscription/request/UnsubscribeWorkbookRequest.kt @@ -1,11 +1,5 @@ package com.few.api.web.controller.subscription.request -import jakarta.validation.constraints.Email -import jakarta.validation.constraints.NotBlank - data class UnsubscribeWorkbookRequest( - @field:NotBlank(message = "{email.notblank}") - @field:Email(message = "{email.invalid}") - val email: String, val opinion: String, ) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt index 55507d5db..c978841bf 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt @@ -4,11 +4,14 @@ import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseIn import com.few.api.domain.workbook.usecase.ReadWorkbookUseCase import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.response.* import com.few.api.web.support.WorkBookCategory import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator import com.few.api.web.support.ViewCategory +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -25,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController class WorkBookController( private val readWorkbookUseCase: ReadWorkbookUseCase, private val browseWorkBooksUseCase: BrowseWorkbooksUseCase, + private val tokenResolver: TokenResolver, ) { @GetMapping("/categories") @@ -44,14 +48,20 @@ class WorkBookController( @GetMapping fun browseWorkBooks( + servletRequest: HttpServletRequest, @RequestParam(value = "category", required = false) category: WorkBookCategory?, @RequestParam(value = "view", required = false) viewCategory: ViewCategory?, ): ApiResponse> { + val authorization: String? = servletRequest.getHeader("Authorization") + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) + } val useCaseOut = - // todo fix memberId - BrowseWorkbooksUseCaseIn(category ?: WorkBookCategory.All, viewCategory, 1L).let { useCaseIn -> + BrowseWorkbooksUseCaseIn(category ?: WorkBookCategory.All, viewCategory, memberId).let { useCaseIn -> browseWorkBooksUseCase.execute(useCaseIn) } diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt index 40fb0abdd..e1cf175ec 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt @@ -2,9 +2,12 @@ package com.few.api.web.controller.workbook.article import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleUseCaseIn import com.few.api.domain.workbook.article.usecase.ReadWorkBookArticleUseCase +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.article.response.ReadWorkBookArticleResponse import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -19,10 +22,12 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping(value = ["/api/v1/workbooks/{workbookId}/articles"], produces = [MediaType.APPLICATION_JSON_VALUE]) class WorkBookArticleController( private val readWorkBookArticleUseCase: ReadWorkBookArticleUseCase, + private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readWorkBookArticle( + servletRequest: HttpServletRequest, @PathVariable(value = "workbookId") @Min(value = 1, message = "{min.id}") workbookId: Long, @@ -30,7 +35,18 @@ class WorkBookArticleController( @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { - val useCaseOut = ReadWorkBookArticleUseCaseIn(workbookId = workbookId, articleId = articleId).let { useCaseIn: ReadWorkBookArticleUseCaseIn -> + val authorization: String? = servletRequest.getHeader("Authorization") + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) + } ?: 0L + + val useCaseOut = ReadWorkBookArticleUseCaseIn( + workbookId = workbookId, + articleId = articleId, + memberId + ).let { useCaseIn: ReadWorkBookArticleUseCaseIn -> readWorkBookArticleUseCase.execute(useCaseIn) } diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt index 2d10346aa..4279a4f10 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/response/ReadWorkBookArticleResponse.kt @@ -1,5 +1,6 @@ package com.few.api.web.controller.workbook.article.response +import com.fasterxml.jackson.annotation.JsonFormat import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleOut import com.few.api.web.controller.workbook.response.WriterInfo import java.time.LocalDateTime @@ -11,6 +12,7 @@ data class ReadWorkBookArticleResponse( val content: String, val problemIds: List, val category: String, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val day: Long, ) { diff --git a/api/src/main/resources/logback-spring.xml b/api/src/main/resources/logback-spring.xml index 757f28ae8..60edecfc1 100644 --- a/api/src/main/resources/logback-spring.xml +++ b/api/src/main/resources/logback-spring.xml @@ -16,16 +16,16 @@ - + - + - + - + diff --git a/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt index e832feb97..d8300cec2 100644 --- a/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt @@ -57,7 +57,8 @@ class ReadArticleUseCaseTest : BehaviorSpec({ val writerSvcOutDto = ReadWriterOutDto( writerId = 1L, name = "hunca", - url = URL("https://jh-labs.tistory.com/") + url = URL("https://jh-labs.tistory.com/"), + imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") ) val probSvcOutDto = BrowseArticleProblemsOutDto(problemIds = listOf(1, 2, 3)) diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt index 0d2049086..116344038 100644 --- a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt @@ -1,8 +1,6 @@ package com.few.api.domain.subscription.usecase import com.few.api.domain.subscription.event.dto.WorkbookSubscriptionEvent -import com.few.api.domain.subscription.service.MemberService -import com.few.api.domain.subscription.service.dto.MemberIdOutDto import com.few.api.domain.subscription.usecase.dto.SubscribeWorkbookUseCaseIn import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.record.WorkbookSubscriptionStatus @@ -20,26 +18,21 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ val log = KotlinLogging.logger {} lateinit var subscriptionDao: SubscriptionDao - lateinit var memberService: MemberService lateinit var applicationEventPublisher: ApplicationEventPublisher lateinit var useCase: SubscribeWorkbookUseCase val workbookId = 1L - val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, email = "test@test.com") + val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = 1L) beforeContainer { subscriptionDao = mockk() - memberService = mockk() applicationEventPublisher = mockk() - useCase = SubscribeWorkbookUseCase(subscriptionDao, memberService, applicationEventPublisher) + useCase = SubscribeWorkbookUseCase(subscriptionDao, applicationEventPublisher) } given("구독 요청이 온 상황에서") { `when`("subscriptionStatus가 null일 경우") { - val serviceOutDto = MemberIdOutDto(memberId = 1L) val event = WorkbookSubscriptionEvent(workbookId) - every { memberService.readMemberId(any()) } returns null - every { memberService.insertMember(any()) } returns serviceOutDto every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns null every { subscriptionDao.insertWorkbookSubscription(any()) } just Runs every { applicationEventPublisher.publishEvent(event) } answers { @@ -49,7 +42,6 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ then("신규 구독을 추가한다") { useCase.execute(useCaseIn) - verify(exactly = 1) { memberService.insertMember(any()) } verify(exactly = 1) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 0) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 0) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } @@ -60,12 +52,9 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ `when`("구독을 취소한 경우") { val day = 2 val lastDay = 3 - val serviceOutDto = MemberIdOutDto(memberId = 1L) val subscriptionStatusRecord = WorkbookSubscriptionStatus(workbookId = workbookId, isActiveSub = false, day) val event = WorkbookSubscriptionEvent(workbookId) - every { memberService.readMemberId(any()) } returns null - every { memberService.insertMember(any()) } returns serviceOutDto every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns subscriptionStatusRecord every { subscriptionDao.countWorkbookMappedArticles(any()) } returns lastDay every { subscriptionDao.reSubscribeWorkbookSubscription(any()) } just Runs @@ -76,7 +65,6 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ then("재구독한다") { useCase.execute(useCaseIn) - verify(exactly = 1) { memberService.insertMember(any()) } verify(exactly = 0) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 1) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 1) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } @@ -87,12 +75,9 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ `when`("이미 구독하고 있을 경우") { val day = 2 val lastDay = 3 - val serviceOutDto = MemberIdOutDto(memberId = 1L) val subscriptionStatusRecord = WorkbookSubscriptionStatus(workbookId = workbookId, isActiveSub = true, day) val event = WorkbookSubscriptionEvent(workbookId) - every { memberService.readMemberId(any()) } returns null - every { memberService.insertMember(any()) } returns serviceOutDto every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns subscriptionStatusRecord every { subscriptionDao.countWorkbookMappedArticles(any()) } returns lastDay every { subscriptionDao.reSubscribeWorkbookSubscription(any()) } just Runs @@ -103,7 +88,6 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ then("예외가 발생한다") { shouldThrow { useCase.execute(useCaseIn) } - verify(exactly = 1) { memberService.insertMember(any()) } verify(exactly = 0) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 0) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 0) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } diff --git a/api/src/test/kotlin/com/few/api/web/controller/TestTokenUserDetailsService.kt b/api/src/test/kotlin/com/few/api/web/controller/TestTokenUserDetailsService.kt new file mode 100644 index 000000000..b55b5600e --- /dev/null +++ b/api/src/test/kotlin/com/few/api/web/controller/TestTokenUserDetailsService.kt @@ -0,0 +1,20 @@ +package com.few.api.web.controller + +import com.few.api.security.authentication.authority.Roles +import com.few.api.security.authentication.token.TokenUserDetails +import org.springframework.boot.test.context.TestComponent +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService + +@TestComponent +class TestTokenUserDetailsService : UserDetailsService { + override fun loadUserByUsername(username: String): UserDetails { + return TokenUserDetails( + authorities = listOf( + Roles.ROLE_USER.authority + ), + id = "1", + email = "test@gmail.com" + ) + } +} \ No newline at end of file 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 f10b48eed..eb9b650e5 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 @@ -315,8 +315,8 @@ class AdminControllerTest : ControllerTestSpec() { val api = "PutImage" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/utilities/conversion/image").build().toUriString() val request = ImageSourceRequest(source = MockMultipartFile("source", "test.jpg", "image/jpeg", "test".toByteArray())) - val response = ImageSourceResponse(URL("http://localhost:8080/test.jpg")) - val useCaseOut = PutImageUseCaseOut(response.url) + val response = ImageSourceResponse(URL("http://localhost:8080/test.jpg"), listOf("jpg", "webp")) + val useCaseOut = PutImageUseCaseOut(response.url, response.supportSuffix) val useCaseIn = PutImageUseCaseIn(request.source) `when`(putImageUseCase.execute(useCaseIn)).thenReturn(useCaseOut) @@ -344,6 +344,10 @@ class AdminControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.url") .fieldWithString( "이미지 URL" + ), + PayloadDocumentation.fieldWithPath("data.supportSuffix") + .fieldWithArray( + "지원하는 확장자" ) ) ) diff --git a/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt index 958ff7075..638e01975 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.few.api.domain.article.usecase.ReadArticleUseCase import com.few.api.domain.article.usecase.ReadArticlesUseCase import com.few.api.domain.article.usecase.dto.* +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* @@ -22,9 +23,13 @@ import org.springframework.http.MediaType import org.springframework.http.codec.json.Jackson2JsonDecoder import org.springframework.http.codec.json.Jackson2JsonEncoder import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder import java.net.URL import java.time.LocalDateTime @@ -36,6 +41,9 @@ class ArticleControllerTest : ControllerTestSpec() { private lateinit var webTestClient: WebTestClient + @Autowired + private lateinit var mockMvc: MockMvc + @Autowired private lateinit var articleController: ArticleController @@ -45,6 +53,9 @@ class ArticleControllerTest : ControllerTestSpec() { @MockBean private lateinit var readArticlesUseCase: ReadArticlesUseCase + @MockBean + private lateinit var tokenResolver: TokenResolver + companion object { private val BASE_URL = "/api/v1/articles" private val TAG = "ArticleController" @@ -63,6 +74,9 @@ class ArticleControllerTest : ControllerTestSpec() { .build() } + /** + * 인증/비인증 모두 가능한 API + */ @Test @DisplayName("[GET] /api/v1/articles/{articleId}") fun readArticle() { @@ -71,14 +85,17 @@ class ArticleControllerTest : ControllerTestSpec() { val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/{articleId}").build().toUriString() // set usecase mock val articleId = 1L - val memberId = 0L + val memberId = 1L + + `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) `when`(readArticleUseCase.execute(ReadArticleUseCaseIn(articleId, memberId))).thenReturn( ReadArticleUseCaseOut( id = 1L, writer = WriterDetail( id = 1L, name = "안나포", - url = URL("http://localhost:8080/api/v1/writers/1") + url = URL("http://localhost:8080/api/v1/writers/1"), + imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") ), mainImageUrl = URL("https://github.com/YAPP-Github/24th-Web-Team-1-BE/assets/102807742/0643d805-5f3a-4563-8c48-2a7d51795326"), title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", @@ -91,14 +108,23 @@ class ArticleControllerTest : ControllerTestSpec() { ) // when - this.webTestClient.get().uri(uri, articleId).accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri, articleId) + .header("Authorization", "Bearer thisisaccesstoken") + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder().description("아티클 Id로 아티클 조회") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + .optional() + ) .pathParameters(parameterWithName("articleId").description("아티클 Id")) .responseSchema(Schema.schema(api.toResponseSchema())).responseFields( *Description.describe( @@ -115,6 +141,8 @@ class ArticleControllerTest : ControllerTestSpec() { .fieldWithString("아티클 작가 이름"), PayloadDocumentation.fieldWithPath("data.writer.url") .fieldWithString("아티클 작가 링크"), + PayloadDocumentation.fieldWithPath("data.writer.imageUrl") + .fieldWithString("아티클 작가 이미지 링크(non-null)"), PayloadDocumentation.fieldWithPath("data.mainImageUrl") .fieldWithString("아티클 썸네일 이미지 링크"), PayloadDocumentation.fieldWithPath("data.title") @@ -161,7 +189,8 @@ class ArticleControllerTest : ControllerTestSpec() { writer = WriterDetail( id = 1L, name = "안나포", - url = URL("http://localhost:8080/api/v1/writers/1") + url = URL("http://localhost:8080/api/v1/writers/1"), + imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") ), mainImageUrl = URL("https://github.com/YAPP-Github/24th-Web-Team-1-BE/assets/102807742/0643d805-5f3a-4563-8c48-2a7d51795326"), title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", @@ -218,6 +247,8 @@ class ArticleControllerTest : ControllerTestSpec() { .fieldWithString("아티클 작가 이름"), PayloadDocumentation.fieldWithPath("data.articles[].writer.url") .fieldWithString("아티클 작가 링크"), + PayloadDocumentation.fieldWithPath("data.articles[].writer.imageUrl") + .fieldWithString("아티클 작가 이미지 링크(non-null)"), PayloadDocumentation.fieldWithPath("data.articles[].mainImageUrl") .fieldWithString("아티클 썸네일 이미지 링크"), PayloadDocumentation.fieldWithPath("data.articles[].title") diff --git a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt index 4cda425f5..7284f0599 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt @@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.few.api.domain.subscription.usecase.BrowseSubscribeWorkbooksUseCase import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description -import com.few.api.web.controller.subscription.request.SubscribeWorkbookRequest import com.few.api.web.controller.subscription.request.UnsubscribeWorkbookRequest import com.few.api.domain.subscription.usecase.SubscribeWorkbookUseCase import com.few.api.domain.subscription.usecase.UnsubscribeAllUseCase @@ -28,9 +27,15 @@ import org.springframework.http.MediaType import org.springframework.http.codec.json.Jackson2JsonDecoder import org.springframework.http.codec.json.Jackson2JsonEncoder import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.security.test.context.support.WithUserDetails import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.web.util.UriComponentsBuilder class SubscriptionControllerTest : ControllerTestSpec() { @@ -40,6 +45,9 @@ class SubscriptionControllerTest : ControllerTestSpec() { private lateinit var webTestClient: WebTestClient + @Autowired + private lateinit var mockMvc: MockMvc + @Autowired private lateinit var subscriptionController: SubscriptionController @@ -75,6 +83,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { @Test @DisplayName("[GET] /api/v1/subscriptions/workbooks") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") fun browseSubscribeWorkbooks() { // given val api = "BrowseSubscribeWorkBooks" @@ -123,12 +132,13 @@ class SubscriptionControllerTest : ControllerTestSpec() { doReturn(useCaseOut).`when`(browseSubscribeWorkbooksUseCase).execute(useCaseIn) // when - this.webTestClient.get() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri) + .header("Authorization", "Bearer thisisaccesstoken") + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() @@ -137,6 +147,12 @@ class SubscriptionControllerTest : ControllerTestSpec() { .privateResource(false) .deprecated(false) .tag(TAG) + .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) .responseSchema(Schema.schema(api.toResponseSchema())) .responseFields( *Description.describe( @@ -170,31 +186,30 @@ class SubscriptionControllerTest : ControllerTestSpec() { @Test @DisplayName("[POST] /api/v1/workbooks/{workbookId}/subs") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") fun subscribeWorkbook() { // given val api = "SubscribeWorkBook" val uri = UriComponentsBuilder.newInstance() - .path("$BASE_URL/workbooks/{workbookId}/subs").build().toUriString() + .path("$BASE_URL/workbooks/{workbookId}/subs") + .build().toUriString() val email = "test@gmail.com" - val body = objectMapper.writeValueAsString(SubscribeWorkbookRequest(email = email)) // set usecase mock - val workbookId = 1L val memberId = 1L - - val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, email = email) + val workbookId = 1L + val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = memberId) doNothing().`when`(subscribeWorkbookUseCase).execute(useCaseIn) // when - this.webTestClient.post() - .uri(uri, workbookId) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri, workbookId) + .header("Authorization", "Bearer thisisaccesstoken") + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() @@ -204,6 +219,11 @@ class SubscriptionControllerTest : ControllerTestSpec() { .deprecated(false) .tag(TAG) .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) .pathParameters(parameterWithName("workbookId").description("학습지 Id")) .responseSchema(Schema.schema(api.toResponseSchema())) .responseFields( @@ -217,38 +237,38 @@ class SubscriptionControllerTest : ControllerTestSpec() { @Test @DisplayName("[POST] /api/v1/workbooks/{workbookId}/unsubs") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") fun unsubscribeWorkbook() { // given val api = "UnsubscribeWorkBook" val uri = UriComponentsBuilder.newInstance() - .path("${SubscriptionControllerTest.BASE_URL}/workbooks/{workbookId}/unsubs") + .path("$BASE_URL/workbooks/{workbookId}/unsubs") .build() .toUriString() // set usecase mock val workbookId = 1L val memberId = 1L - val email = "test@gmail.com" val opinion = "취소합니다." val body = objectMapper.writeValueAsString( - UnsubscribeWorkbookRequest(email = email, opinion = opinion) + UnsubscribeWorkbookRequest(opinion = opinion) ) val useCaseIn = UnsubscribeWorkbookUseCaseIn( workbookId = workbookId, - email = email, + memberId = memberId, opinion = opinion ) doNothing().`when`(unsubscribeWorkbookUseCase).execute(useCaseIn) // when - this.webTestClient.post() - .uri(uri, workbookId) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri, workbookId) + .header("Authorization", "Bearer thisisaccesstoken") + .contentType(MediaType.APPLICATION_JSON) + .content(body) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() @@ -259,6 +279,11 @@ class SubscriptionControllerTest : ControllerTestSpec() { .tag(TAG) .requestSchema(Schema.schema(api.toRequestSchema())) .pathParameters(parameterWithName("workbookId").description("학습지 Id")) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) .responseSchema(Schema.schema(api.toResponseSchema())) .responseFields( *Description.describe() @@ -271,6 +296,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { @Test @DisplayName("[POST] /api/v1/subscriptions/unsubs") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") fun deactivateAllSubscriptions() { // given val api = "UnsubscribeAll" @@ -281,26 +307,25 @@ class SubscriptionControllerTest : ControllerTestSpec() { // set usecase mock val memberId = 1L - val email = "test@gmail.com" val opinion = "전체 수신거부합니다." val body = objectMapper.writeValueAsString( - UnsubscribeWorkbookRequest(email = email, opinion = opinion) + UnsubscribeWorkbookRequest(opinion = opinion) ) val useCaseIn = UnsubscribeAllUseCaseIn( - email = email, + memberId = memberId, opinion = opinion ) doNothing().`when`(unsubscribeAllUseCase).execute(useCaseIn) // when - this.webTestClient.post() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri) + .header("Authorization", "Bearer thisisaccesstoken") + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() @@ -310,6 +335,11 @@ class SubscriptionControllerTest : ControllerTestSpec() { .deprecated(false) .tag(TAG) .requestSchema(Schema.schema(api.toRequestSchema())) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + ) .responseSchema(Schema.schema(api.toResponseSchema())) .responseFields( *Description.describe() diff --git a/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt index afc644c52..0fc453adb 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.ReadWorkbookUseCase import com.few.api.domain.workbook.usecase.dto.* +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* @@ -55,6 +56,9 @@ class WorkBookControllerTest : ControllerTestSpec() { @MockBean private lateinit var browseWorkBooksUseCase: BrowseWorkbooksUseCase + @MockBean + private lateinit var tokenResolver: TokenResolver + companion object { private val BASE_URL = "/api/v1/workbooks" private val TAG = "WorkBookController" @@ -117,6 +121,9 @@ class WorkBookControllerTest : ControllerTestSpec() { ) } + /** + * 인증/비인증 모두 가능한 API + */ @Test @DisplayName("[GET] /api/v1/workbooks") fun browseWorkBooks() { @@ -132,6 +139,7 @@ class WorkBookControllerTest : ControllerTestSpec() { .toUriString() // set usecase mock + `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) `when`(browseWorkBooksUseCase.execute(BrowseWorkbooksUseCaseIn(WorkBookCategory.ECONOMY, viewCategory, memberId))).thenReturn( BrowseWorkbooksUseCaseOut( workbooks = listOf( @@ -156,6 +164,7 @@ class WorkBookControllerTest : ControllerTestSpec() { // when mockMvc.perform( get(uri) + .header("Authorization", "Bearer thisisaccesstoken") .contentType(MediaType.APPLICATION_JSON) ).andExpect( status().is2xxSuccessful @@ -172,6 +181,12 @@ class WorkBookControllerTest : ControllerTestSpec() { parameterWithName("category").description("학습지 카테고리").optional(), parameterWithName("view").description("뷰 카테고리").optional() ) + .requestHeaders( + ResourceDocumentation.headerWithName("Authorization") + .defaultValue("{{accessToken}}") + .description("Bearer 어세스 토큰") + .optional() + ) .responseSchema(Schema.schema(api.toResponseSchema())).responseFields( *Description.describe( arrayOf( diff --git a/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt index 4b6ef37e8..976fcfcd9 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt @@ -9,6 +9,7 @@ import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleUseCaseIn import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleOut import com.few.api.domain.workbook.article.dto.WriterDetail import com.few.api.domain.workbook.article.usecase.ReadWorkBookArticleUseCase +import com.few.api.security.token.TokenResolver import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* @@ -18,14 +19,18 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.http.MediaType import org.mockito.Mockito.`when` import org.springframework.http.codec.json.Jackson2JsonDecoder import org.springframework.http.codec.json.Jackson2JsonEncoder import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import org.springframework.restdocs.payload.PayloadDocumentation import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation +import org.springframework.security.test.context.support.WithUserDetails import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.web.util.UriComponentsBuilder import java.net.URL import java.time.LocalDateTime @@ -37,12 +42,18 @@ class WorkBookArticleControllerTest : ControllerTestSpec() { private lateinit var webTestClient: WebTestClient + @Autowired + private lateinit var mockMvc: MockMvc + @Autowired private lateinit var workBookArticleController: WorkBookArticleController @MockBean private lateinit var readWorkBookArticleUseCase: ReadWorkBookArticleUseCase + @MockBean + private lateinit var tokenResolver: TokenResolver + companion object { private val BASE_URL = "/api/v1/workbooks/{workbookId}/articles" private val TAG = "WorkBookArticleController" @@ -63,6 +74,7 @@ class WorkBookArticleControllerTest : ControllerTestSpec() { @Test @DisplayName("[GET] /api/v1/workbooks/{workbookId}/articles/{articleId}") + @WithUserDetails(userDetailsServiceBeanName = "testTokenUserDetailsService") fun readWorkBookArticle() { // given val api = "ReadWorkBookArticle" @@ -73,7 +85,9 @@ class WorkBookArticleControllerTest : ControllerTestSpec() { // set usecase mock val workbookId = 1L val articleId = 1L - `when`(readWorkBookArticleUseCase.execute(ReadWorkBookArticleUseCaseIn(workbookId, articleId))).thenReturn( + val memberId = 1L + `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) + `when`(readWorkBookArticleUseCase.execute(ReadWorkBookArticleUseCaseIn(workbookId, articleId, memberId = memberId))).thenReturn( ReadWorkBookArticleOut( id = 1L, writer = WriterDetail( @@ -91,12 +105,12 @@ class WorkBookArticleControllerTest : ControllerTestSpec() { ) // when - this.webTestClient.get() - .uri(uri, workbookId, articleId) - .accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri, workbookId, articleId) + .header("Authorization", "Bearer thisisaccesstoken") + ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) + .andDo( + MockMvcRestDocumentation.document( api.toIdentifier(), ResourceDocumentation.resource( ResourceSnippetParameters.builder() diff --git a/buildSrc/src/main/kotlin/DependencyVersion.kt b/buildSrc/src/main/kotlin/DependencyVersion.kt index bd2186d61..26de6fd7d 100644 --- a/buildSrc/src/main/kotlin/DependencyVersion.kt +++ b/buildSrc/src/main/kotlin/DependencyVersion.kt @@ -14,6 +14,9 @@ object DependencyVersion { /** jwt */ const val JWT = "0.11.5" + /** scrimage */ + const val SCRIMAGE = "4.1.2" + /** flyway */ const val FLYWAY = "9.16.0" diff --git a/data/db/migration/entity/V1.00.0.16__drop_subscription_unique_constraint.sql b/data/db/migration/entity/V1.00.0.16__drop_subscription_unique_constraint.sql new file mode 100644 index 000000000..f3e16a62a --- /dev/null +++ b/data/db/migration/entity/V1.00.0.16__drop_subscription_unique_constraint.sql @@ -0,0 +1,6 @@ +-- Subscription의 unique constraint를 삭제합니다. +ALTER TABLE SUBSCRIPTION + DROP CONSTRAINT subscription_unique_member_id_target_member_id; + +ALTER TABLE SUBSCRIPTION + DROP CONSTRAINT subscription_unique_member_id_target_workbook_id; diff --git a/data/db/migration/entity/V1.00.0.17__add_subscription_target_workbook_idx.sql b/data/db/migration/entity/V1.00.0.17__add_subscription_target_workbook_idx.sql new file mode 100644 index 000000000..a72ff62dd --- /dev/null +++ b/data/db/migration/entity/V1.00.0.17__add_subscription_target_workbook_idx.sql @@ -0,0 +1,3 @@ +-- Subscription의 target_workbook_id에 대한 index를 추가합니다. +CREATE INDEX subscription_target_workbook_idx + ON SUBSCRIPTION (target_workbook_id); diff --git a/data/db/migration/entity/V1.00.0.18__add_subscription_member_idx.sql b/data/db/migration/entity/V1.00.0.18__add_subscription_member_idx.sql new file mode 100644 index 000000000..b8d21fdb0 --- /dev/null +++ b/data/db/migration/entity/V1.00.0.18__add_subscription_member_idx.sql @@ -0,0 +1,3 @@ +-- Subscription의 member_id에 대한 index를 추가합니다. +CREATE INDEX subscription_member_idx + ON SUBSCRIPTION (member_id); diff --git a/data/db/migration/entity/V1.00.0.19__add_workbook_category_idx.sql b/data/db/migration/entity/V1.00.0.19__add_workbook_category_idx.sql new file mode 100644 index 000000000..3baa825bf --- /dev/null +++ b/data/db/migration/entity/V1.00.0.19__add_workbook_category_idx.sql @@ -0,0 +1,3 @@ +-- Workbook의 category_cd를 위한 인덱스 추가 +CREATE INDEX workbook_category_idx + ON WORKBOOK (category_cd); diff --git a/data/db/migration/entity/V1.00.0.20__add_maaping_workbook_article_idx.sql b/data/db/migration/entity/V1.00.0.20__add_maaping_workbook_article_idx.sql new file mode 100644 index 000000000..efd7bbce0 --- /dev/null +++ b/data/db/migration/entity/V1.00.0.20__add_maaping_workbook_article_idx.sql @@ -0,0 +1,3 @@ +-- MAPPING_WORKBOOK_ARTICLE 테이블에 인덱스 추가 +CREATE INDEX mapping_workbook_article_workbook_id_idx + ON MAPPING_WORKBOOK_ARTICLE (workbook_id); diff --git a/email/src/main/kotlin/com/few/email/service/member/SendAuthEmailService.kt b/email/src/main/kotlin/com/few/email/service/member/SendAuthEmailService.kt index b82575bf9..096170d8c 100644 --- a/email/src/main/kotlin/com/few/email/service/member/SendAuthEmailService.kt +++ b/email/src/main/kotlin/com/few/email/service/member/SendAuthEmailService.kt @@ -20,6 +20,8 @@ class SendAuthEmailService( val context = Context() context.setVariable("email", args.content.email) context.setVariable("confirmLink", args.content.confirmLink) + context.setVariable("headComment", args.content.headComment) + context.setVariable("subComment", args.content.subComment) return templateEngine.process(args.template, context) } } \ No newline at end of file diff --git a/email/src/main/kotlin/com/few/email/service/member/dto/SendAuthEmailArgs.kt b/email/src/main/kotlin/com/few/email/service/member/dto/SendAuthEmailArgs.kt index 07748932d..1b40a0892 100644 --- a/email/src/main/kotlin/com/few/email/service/member/dto/SendAuthEmailArgs.kt +++ b/email/src/main/kotlin/com/few/email/service/member/dto/SendAuthEmailArgs.kt @@ -12,6 +12,8 @@ data class SendAuthEmailArgs( ) : SendMailArgs data class Content( + val headComment: String, + val subComment: String, val email: String, val confirmLink: URL, ) \ No newline at end of file diff --git a/email/src/main/resources/templates/auth.html b/email/src/main/resources/templates/auth.html index 7d2af8de2..318114af7 100644 --- a/email/src/main/resources/templates/auth.html +++ b/email/src/main/resources/templates/auth.html @@ -89,7 +89,7 @@ few_logo @@ -99,7 +99,7 @@
-

few에 가입해주셔서 감사합니다.

+

@@ -107,7 +107,7 @@

few에 가입해주셔
- 가입하신 이메일 주소를 확인해주세요. +
@@ -120,12 +120,11 @@

few에 가입해주셔 id="article-to-problem" class="body3-medium" style=" - display: flex; + display: block; height: 20px; text-decoration: none; - padding: 17px 0px 18px 0px; - justify-content: center; - align-items: center; + padding: 17px 0; + text-align: center; color: #ffffff; background-color: #264932; "