diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml index c497fa7..baa1d6a 100644 --- a/.github/workflows/dev_deploy.yml +++ b/.github/workflows/dev_deploy.yml @@ -17,35 +17,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - - name: Set up JDK 17 - uses: actions/setup-java@v1 - with: - java-version: 17 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - shell: bash - - - name: Build with Gradle - run: ./gradlew build - shell: bash - name: Make Directory for deliver run: mkdir deploy - - name: Copy Jar - run: cp ./module-web/build/libs/*.jar ./deploy/ - # appspec.yml Copy - name: Copy appspec run: cp ./appspec.yml ./deploy/ # Dockerfile Copy - name: Copy Dockerfile - run: | - cp ./dockerfile ./deploy/ - cp ./docker-compose.yml ./deploy/ + run: cp ./docker-compose.yml ./deploy/ # script file Copy - name: Copy shell @@ -53,9 +35,6 @@ jobs: mkdir deploy/scripts cp ./scripts/* ./deploy/scripts/ - - name: Copy Jar - run: cp ./module-web/build/libs/*.jar ./deploy/ - - name: Make zip file run: zip -r ./$GITHUB_SHA.zip ./deploy/ shell: bash @@ -67,6 +46,11 @@ jobs: aws-secret-access-key: ${{ secrets.DEV_AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} + - name: Setup CodeBuild + uses: aws-actions/aws-codebuild-run-build@v1.0.3 + with: + project-name: ygtang-develop-build + - name: Upload to S3 run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_LOCATION/$PROJECT_NAME/$GITHUB_SHA.zip diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9804e68..0d5772f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -9,6 +9,7 @@ on: pull_request: branches: - 'develop' + types: [opened, synchronize, reopened] jobs: build: @@ -17,16 +18,24 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Set up JDK 17 uses: actions/setup-java@v1 with: java-version: 17 - - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew - shell: bash - - - name: Build with Gradle - run: ./gradlew build - shell: bash + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: chmod +x ./gradlew && ./gradlew build sonarqube --info diff --git a/.github/workflows/prod_deploy.yml b/.github/workflows/prod_deploy.yml index 6e8c5bf..beaaddc 100644 --- a/.github/workflows/prod_deploy.yml +++ b/.github/workflows/prod_deploy.yml @@ -17,35 +17,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - - name: Set up JDK 17 - uses: actions/setup-java@v1 - with: - java-version: 17 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - shell: bash - - - name: Build with Gradle - run: ./gradlew build - shell: bash - name: Make Directory for deliver run: mkdir deploy - - name: Copy Jar - run: cp ./module-web/build/libs/*.jar ./deploy/ - # appspec.yml Copy - name: Copy appspec run: cp ./appspec.yml ./deploy/ # Dockerfile Copy - name: Copy Dockerfile - run: | - cp ./dockerfile ./deploy/ - cp ./docker-compose.yml ./deploy/ + run: cp ./docker-compose.yml ./deploy/ # script file Copy - name: Copy shell @@ -53,16 +35,21 @@ jobs: mkdir deploy/scripts cp ./scripts/* ./deploy/scripts/ - - name: Make zip file + - name: Make zip file run: zip -r ./$GITHUB_SHA.zip ./deploy/ - shell: bash + shell: bash - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Setup CodeBuild + uses: aws-actions/aws-codebuild-run-build@v1.0.3 + with: + project-name: ygtang-build-production - name: Upload to S3 run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_LOCATION/$PROJECT_NAME/$GITHUB_SHA.zip diff --git a/README.md b/README.md index b3a9c2c..96aa87a 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ 고은정 - + @@ -142,7 +142,7 @@ 오혜성 - + @@ -152,7 +152,7 @@ 정대윤 - + @@ -162,21 +162,9 @@ 정도현 - - - - -
- Design 🎨 -
- 김자영 -
- - - - - - + + + @@ -186,7 +174,7 @@ 김자연 - + @@ -196,7 +184,7 @@ 문인우 - + @@ -206,7 +194,30 @@ 정형일 - + + + +
+ BE 💾 +
+ 전해성 +
+ + + + + + + + + + +
+ Design 🎨 +
+ 김자영 +
+ @@ -216,7 +227,7 @@ 박수연 - + @@ -228,5 +239,5 @@ - + diff --git a/appspec.yml b/appspec.yml index 9d97e94..f7a9611 100644 --- a/appspec.yml +++ b/appspec.yml @@ -16,12 +16,9 @@ permissions: hooks: ApplicationStart: - - location: scripts/run_new_was.sh + - location: scripts/deploy.sh timeout: 200 runas: ubuntu - location: scripts/health_check.sh timeout: 200 - runas: ubuntu - #- location: scripts/switch.sh - # timeout: 180 - #cd bu runas: ubuntu + runas: ubuntu \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4c6bf51..dece43f 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,15 @@ buildscript { } plugins { - id "com.gorylenko.gradle-git-properties" version "$gradleGitPropertiesPluginVersion" apply false + id "org.sonarqube" version "$sonarqubeVersion" +} + +sonarqube { + properties { + property "sonar.projectKey", "ygtang-server" + property "sonar.organization", "yeonggamt" + property "sonar.host.url", "https://sonarcloud.io" + } } subprojects { @@ -23,6 +31,11 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'java' apply plugin: 'java-library' + apply plugin: 'jacoco' + + jacoco { + toolVersion = "$jacocoVersion" + } group = 'com.depromeet.inspiration' version = '0.0.1' @@ -49,5 +62,14 @@ subprojects { } test { useJUnitPlatform() + finalizedBy jacocoTestReport + } + jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.required = false + } } } diff --git a/docker-compose.yml b/docker-compose.yml index e6897cf..7e060cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,41 +1,13 @@ -version: "3" +version: "3.9" services: - spring1: - build: . - image: ${PROJECT_NAME}-${VERSION} + app: + image: "${IMAGE_REPO_NAME}:${IMAGE_TAG}" container_name: ${ACTIVE} ports: - "8080:8080" - "8081:8081" - "5005:5005" volumes: - - ./:/root/ + - "${YGTANG_LOG_DIR}:/logs" environment: - - "JAVA_OPTS=${JAVA_OPTS}" - - "PROJECT_NAME=${PROJECT_NAME}" - - "VERSION=${VERSION}" - - "DEV_PORT=${DEV_PORT}" - - "ACTIVE=${ACTIVE}" - - "ERROR_LOG_POST_SLACK_URL=${ERROR_LOG_POST_SLACK_URL}" - - "DB_HOST=${DB_HOST}" - - "DB_PORT=${DB_PORT}" - - "DB_USER=${DB_USER}" - - "DB_PASSWORD=${DB_PASSWORD}" - - "REDIS_HOST=${REDIS_HOST}" - - "REDIS_PORT=${REDIS_PORT}" - - "MAIL_HOST=${MAIL_HOST}" - - "MAIL_PORT=${MAIL_PORT}" - - "MAIL_USERNAME=${MAIL_USERNAME}" - - "MAIL_PASSWORD=${MAIL_PASSWORD}" - - "SIGN_UP_EMAIL_SEND_MAIL=${SIGN_UP_EMAIL_SEND_MAIL}" - - "RESET_PASSWORD_FOR_AUTH_SEND_MAIL=${RESET_PASSWORD_FOR_AUTH_SEND_MAIL}" - - "AUTH_TOKEN=${AUTH_TOKEN}" - - "POLICY_URL=${POLICY_URL}" - - "SECRET_KEY=${SECRET_KEY}" - - "S3_ACCESS_KEY=${S3_ACCESS_KEY}" - - "S3_SECRET_KEY=${S3_SECRET_KEY}" - - "S3_REGION=${S3_REGION}" - - "S3_BUCKET=${S3_BUCKET}" - - "YGTANG_SERVER_HOST=${YGTANG_SERVER_HOST}" - - + - "JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dspring.profiles.active=${ACTIVE} -Duser.timezone=Asia/Seoul -Djasypt.encryptor.password=${ENC_PASSWORD} -Dserver.tomcat.accesslog.enabled=true -Dserver.tomcat.basedir=/ -Dlogging.level.org.apache.catalina.core=OFF -Dlogging.file.name=/logs/spring.log" diff --git a/gradle.properties b/gradle.properties index 2a75f5e..ed3eb15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,6 @@ apacheCommonsCsvVersion=1.9.0 slackSdkVersion=1.24.0 okhttpVersion=4.10.0 awsJavaSdkVersion=1.12.281 -jasyptVersion=3.0.4 \ No newline at end of file +jasyptVersion=3.0.4 +sonarqubeVersion=3.4.0.2513 +jacocoVersion=0.8.8 \ No newline at end of file diff --git a/module-api/src/main/java/inspiration/aws/AwsS3Config.java b/module-api/src/main/java/inspiration/aws/AwsS3Config.java index c2effef..19990bd 100644 --- a/module-api/src/main/java/inspiration/aws/AwsS3Config.java +++ b/module-api/src/main/java/inspiration/aws/AwsS3Config.java @@ -1,5 +1,6 @@ package inspiration.aws; +import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; @@ -11,6 +12,7 @@ @Configuration public class AwsS3Config { + @Value("${cloud.aws.credentials.access-key}") private String accessKey; @@ -20,12 +22,22 @@ public class AwsS3Config { @Value("${cloud.aws.region.static}") private String region; + @Value("${cloud.aws.s3.connection-timeout:1000}") + private int connectionTimeout; + + @Value("${cloud.aws.s3.request-timeout:3000}") + private int requestTimeout; + @Bean public AmazonS3 amazonS3() { AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setConnectionTimeout(connectionTimeout); + clientConfiguration.setRequestTimeout(requestTimeout); return AmazonS3ClientBuilder.standard() .withRegion(region) .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .withClientConfiguration(clientConfiguration) .build(); } diff --git a/module-api/src/main/java/inspiration/domain/inspiration/InspirationService.java b/module-api/src/main/java/inspiration/domain/inspiration/InspirationService.java index 1a31600..2edab22 100644 --- a/module-api/src/main/java/inspiration/domain/inspiration/InspirationService.java +++ b/module-api/src/main/java/inspiration/domain/inspiration/InspirationService.java @@ -2,9 +2,6 @@ import inspiration.RestPage; import inspiration.aws.AwsS3Service; -import inspiration.exception.ConflictRequestException; -import inspiration.exception.NoAccessAuthorizationException; -import inspiration.exception.ResourceNotFoundException; import inspiration.domain.inspiration.opengraph.OpenGraphService; import inspiration.domain.inspiration.opengraph.OpenGraphVo; import inspiration.domain.inspiration.request.InspirationAddRequest; @@ -20,21 +17,25 @@ import inspiration.domain.tag.Tag; import inspiration.domain.tag.TagRepository; import inspiration.domain.tag.TagService; +import inspiration.exception.ConflictRequestException; +import inspiration.exception.NoAccessAuthorizationException; +import inspiration.exception.ResourceNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Slf4j @@ -51,28 +52,23 @@ public class InspirationService { private final InspirationTagRepository inspirationTagRepository; private final TagRepository tagRepository; private final OpenGraphService openGraphService; + private final ThreadPoolTaskExecutor threadPoolTaskExecutor; @Transactional(readOnly = true) - @Cacheable(value = "inspiration", key = "{#memberId, #pageable.pageNumber, #pageable.pageSize}") public RestPage findInspirations(Pageable pageable, Long memberId) { Member member = memberService.findById(memberId); Page inspirationPage = inspirationRepository.findAllByMember(member, pageable); - inspirationPage - .forEach( - inspiration -> - inspiration.setFilePath(getFilePath(inspiration.getType(), inspiration.getContent()))); - return new RestPage<>(inspirationPage.map(inspiration -> InspirationResponse.of(inspiration, getOpenGraphResponse(inspiration.getType(), inspiration.getContent())))); + return toRestPage(inspirationPage); } @Transactional(readOnly = true) - @Cacheable(value = "inspiration", key = "{#memberId, #id}") public InspirationResponse findInspiration(Long id, Long memberId) { Member member = memberService.findById(memberId); Inspiration inspiration = inspirationRepository.findAllByMemberAndId(member, id) - .orElseThrow(ResourceNotFoundException::new); + .orElseThrow(ResourceNotFoundException::new); inspiration.setFilePath(getFilePath(inspiration.getType(), inspiration.getContent())); return InspirationResponse.of(inspiration, getOpenGraphResponse(inspiration.getType(), inspiration.getContent())); @@ -101,8 +97,7 @@ public OpenGraphResponse getOpenGraphResponse(String link) { return getOpenGraphResponse(InspirationType.LINK, link); } - @CacheEvict(value = "inspiration", allEntries = true) - public Long addInspiration(InspirationAddRequest request, Long memberId) { + public Long addInspiration(InspirationAddRequest request, Long memberId) { Member member = memberService.findById(memberId); @@ -110,7 +105,7 @@ public Long addInspiration(InspirationAddRequest request, Long memberId) { tmpInspiration.writeBy(member); if (request.getType() == InspirationType.IMAGE) { - if(request.getFile() == null){ + if (request.getFile() == null) { throw new IllegalArgumentException("IMAGE 타입은 파일을 업로드 해야합니다."); } fileUpload(tmpInspiration, List.of(request.getFile())); @@ -119,8 +114,8 @@ public Long addInspiration(InspirationAddRequest request, Long memberId) { if (request.getTagIds() != null) { List tags = request.getTagIds().stream() - .map(tagService::getTag) - .collect(Collectors.toList()); + .map(tagService::getTag) + .collect(Collectors.toList()); tags.forEach(tag -> inspirationTagService.save(InspirationTag.of(inspiration, tag))); } return inspiration.getId(); @@ -128,27 +123,15 @@ public Long addInspiration(InspirationAddRequest request, Long memberId) { @Transactional(readOnly = true) public RestPage findInspirationsByTags(List tagIds, List types, - LocalDateTime createdDateTimeFrom, LocalDateTime createdDateTimeTo, + LocalDate createdDateTimeFrom, LocalDate createdDateTimeTo, Long memberId, Pageable pageable) { Page inspirationPage = inspirationRepository.findDistinctByMemberIdAndTagIdInAndTypeAndCreatedDateTimeBetween(memberId, tagIds, types, createdDateTimeFrom, createdDateTimeTo, pageable); - inspirationPage - .forEach( - inspiration -> - inspiration.setFilePath(getFilePath(inspiration.getType(), inspiration.getContent())) - ); - - return new RestPage<>( - inspirationPage.map(inspiration -> InspirationResponse.of( - inspiration, - getOpenGraphResponse(inspiration.getType() ,inspiration.getContent())) - ) - ); + return toRestPage(inspirationPage); } - @CacheEvict(value = "inspiration", allEntries = true) public Long modifyMemo(InspirationModifyRequest request, Long memberId) { Inspiration inspiration = getInspiration(request.getId()); @@ -161,7 +144,6 @@ public Long modifyMemo(InspirationModifyRequest request, Long memberId) { } @Transactional - @CacheEvict(value = "inspiration", allEntries = true) public void removeInspiration(Long id, Long memberId) { Inspiration inspiration = getInspiration(id); @@ -182,7 +164,6 @@ public void removeInspiration(Long id, Long memberId) { } @Transactional - @CacheEvict(value = "inspiration", allEntries = true) public void removeAllInspiration(Long memberId) { Member member = memberService.findById(memberId); @@ -208,7 +189,6 @@ public void removeAllInspiration(Long memberId) { } @Transactional(isolation = Isolation.SERIALIZABLE) - @CacheEvict(value = "inspiration", allEntries = true) public Long tagInspiration(InspirationTagRequest request, Long memberId) { Inspiration inspiration = getInspiration(request.getId()); @@ -229,7 +209,6 @@ public Long tagInspiration(InspirationTagRequest request, Long memberId) { return inspiration.getId(); } - @CacheEvict(value = "inspiration", allEntries = true) public void unTagInspiration(Long id, Long tagId, Long memberId) { Inspiration inspiration = getInspiration(id); @@ -249,10 +228,9 @@ public void unTagInspiration(Long id, Long tagId, Long memberId) { } - @CacheEvict(value = "inspiration", allEntries = true) public void unTagInspirationByInspiration(Long id, Long memberId) { - if(!getInspiration(id).getMember().isSameMember(memberId)) { + if (!getInspiration(id).getMember().isSameMember(memberId)) { throw new NoAccessAuthorizationException(); } @@ -261,12 +239,11 @@ public void unTagInspirationByInspiration(Long id, Long memberId) { inspirationTagService.deleteAllByInspiration(inspiration); } - @CacheEvict(value = "inspiration", allEntries = true) public void unTagInspirationByTag(Long tagId, Long memberId) { Tag tag = tagService.getTag(tagId); - if(!tag.getMember().isSameMember(memberId)) { + if (!tag.getMember().isSameMember(memberId)) { throw new NoAccessAuthorizationException(); } @@ -276,13 +253,13 @@ public void unTagInspirationByTag(Long tagId, Long memberId) { private void fileUpload(Inspiration inspiration, List multipartFiles) { List fileNames = awsS3Service.uploadFile(multipartFiles); - if(!fileNames.isEmpty()){ + if (!fileNames.isEmpty()) { inspiration.setFilePath(fileNames.get(0)); } } private String getFilePath(InspirationType type, String content) { - if(type == InspirationType.IMAGE){ + if (type == InspirationType.IMAGE) { return awsS3Service.getFilePath(content); } return content; @@ -292,4 +269,19 @@ private Inspiration getInspiration(Long id) { return inspirationRepository.findById(id) .orElseThrow(ResourceNotFoundException::new); } + + private RestPage toRestPage(Page inspirationPage) { + return new RestPage<>( + inspirationPage.stream() + .parallel() + .peek(it -> it.setFilePath(getFilePath(it.getType(), it.getContent()))) + .map(it -> (Callable) () -> InspirationResponse.of(it, getOpenGraphResponse(it.getType(), it.getContent()))) + .map(it -> threadPoolTaskExecutor.submitListenable(it).completable()) + .map(CompletableFuture::join) + .collect(Collectors.toList()), + inspirationPage.getPageable().getPageNumber(), + inspirationPage.getPageable().getPageSize(), + inspirationPage.getTotalElements() + ); + } } diff --git a/module-api/src/main/java/inspiration/domain/inspiration/opengraph/YgtangOgMetaElementHtmlParser.java b/module-api/src/main/java/inspiration/domain/inspiration/opengraph/YgtangOgMetaElementHtmlParser.java index d3293f0..b472d2a 100644 --- a/module-api/src/main/java/inspiration/domain/inspiration/opengraph/YgtangOgMetaElementHtmlParser.java +++ b/module-api/src/main/java/inspiration/domain/inspiration/opengraph/YgtangOgMetaElementHtmlParser.java @@ -20,7 +20,7 @@ public class YgtangOgMetaElementHtmlParser implements OgMetaElementHtmlParser { public List getOgMetaElementsFrom(String url) { try { final Document document = Jsoup.connect(url) - .timeout(1000) + .timeout(2000) .get(); final Elements metaElements = document.select("meta"); List ogMetaElements = metaElements.stream() diff --git a/module-api/src/main/java/inspiration/domain/tag/TagService.java b/module-api/src/main/java/inspiration/domain/tag/TagService.java index 0018b1b..1f24b50 100644 --- a/module-api/src/main/java/inspiration/domain/tag/TagService.java +++ b/module-api/src/main/java/inspiration/domain/tag/TagService.java @@ -27,7 +27,6 @@ public class TagService { private final MemberService memberService; @Transactional(readOnly = true) - @Cacheable(value = "tag", key = "{#memberId, #pageable.pageNumber, #pageable.pageSize}") public RestPage findTags(Pageable pageable, Long memberId) { Member member = memberService.findById(memberId); @@ -54,7 +53,6 @@ public Page searchTags(Pageable pageable, String keyword, Long memb return new RestPage<>(tagPage.map(TagResponse::from)); } - @CacheEvict(value = "tag", allEntries = true) public TagResponse addTag(TagAddRequest request, Long memberId) { Member member = memberService.findById(memberId); @@ -74,7 +72,6 @@ public Tag getTag(Long id) { .orElseThrow(ResourceNotFoundException::new); } - @CacheEvict(value = "tag", allEntries = true) public void removeTag(Long id, Long memberId) { Tag tag = tagRepository.findById(id) .orElseThrow(ResourceNotFoundException::new); @@ -85,7 +82,6 @@ public void removeTag(Long id, Long memberId) { tagRepository.delete(tag); } - @CacheEvict(value = "tag", allEntries = true) public void removeAllTag(Long memberId) { Member member = memberService.findById(memberId); diff --git a/module-batch/build.gradle b/module-batch/build.gradle index ce2c6f1..b9694e2 100644 --- a/module-batch/build.gradle +++ b/module-batch/build.gradle @@ -3,6 +3,7 @@ dependencies { implementation project(':module-api') implementation 'org.springframework.boot:spring-boot-starter-batch' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework:spring-web' implementation "org.apache.commons:commons-csv:$apacheCommonsCsvVersion" implementation "com.slack.api:slack-api-client:$slackSdkVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" diff --git a/module-batch/src/main/java/inspiration/application/tag/TagGroup.java b/module-batch/src/main/java/inspiration/application/tag/TagGroup.java new file mode 100644 index 0000000..b132f93 --- /dev/null +++ b/module-batch/src/main/java/inspiration/application/tag/TagGroup.java @@ -0,0 +1,29 @@ +package inspiration.application.tag; + +import org.springframework.util.Assert; + +import java.util.List; + +@SuppressWarnings("ClassCanBeRecord") +public class TagGroup { + private final String name; + private final List candidates; + + private TagGroup(String name, List candidates) { + this.name = name; + this.candidates = candidates; + } + + public static TagGroup from(List candidates) { + Assert.notEmpty(candidates, "'candidates' must not be null or empty"); + return new TagGroup(candidates.get(0), candidates); + } + + public String getName() { + return name; + } + + public boolean contains(String name) { + return candidates.stream().anyMatch(it -> it.equalsIgnoreCase(name)); + } +} diff --git a/module-batch/src/main/java/inspiration/application/tag/TagGroupService.java b/module-batch/src/main/java/inspiration/application/tag/TagGroupService.java new file mode 100644 index 0000000..7cd8ca0 --- /dev/null +++ b/module-batch/src/main/java/inspiration/application/tag/TagGroupService.java @@ -0,0 +1,7 @@ +package inspiration.application.tag; + +import java.util.List; + +public interface TagGroupService { + List getTagGroups(); +} diff --git a/module-batch/src/main/java/inspiration/application/tag/TagRankingConfig.java b/module-batch/src/main/java/inspiration/application/tag/TagRankingConfig.java index 16c5055..35f4739 100644 --- a/module-batch/src/main/java/inspiration/application/tag/TagRankingConfig.java +++ b/module-batch/src/main/java/inspiration/application/tag/TagRankingConfig.java @@ -51,6 +51,7 @@ public class TagRankingConfig { private final JobBuilderFactory jobBuilderFactory; private final StepBuilderFactory stepBuilderFactory; private final InspirationTagRepository inspirationTagRepository; + private final TagGroupService googleSheetTagGroupService; @Value("${ygtang.slack.token.bot}") private String botToken; @@ -78,20 +79,21 @@ public Step tagRankingStep() { @StepScope public Tasklet tagRankingTasklet() { return (contribution, chunkContext) -> { - List tagRankingVoList = getTagRankingVoList(); + List tagGroups = googleSheetTagGroupService.getTagGroups(); + List tagRankingVoList = getTagRankingVoList(tagGroups); File csvFile = toCsvFile(tagRankingVoList); sendFileToSlack(csvFile); return RepeatStatus.FINISHED; }; } - private List getTagRankingVoList() { + private List getTagRankingVoList(List tagGroups) { List inspirationTags = inspirationTagRepository.findAll(); Map> contentTagIdSetMap = inspirationTags.stream() .collect(Collectors.toMap( - it -> it.getTag().getContent(), + it -> resolveTagName(it.getTag().getContent(), tagGroups), it -> Stream.of(it.getTag().getId()).collect(Collectors.toSet()), (a, b) -> { Set c = new HashSet<>(a); @@ -102,7 +104,7 @@ private List getTagRankingVoList() { Map> contentInspirationIdSetMap = inspirationTags.stream() .collect(Collectors.toMap( - it -> it.getTag().getContent(), + it -> resolveTagName(it.getTag().getContent(), tagGroups), it -> Stream.of(it.getInspiration().getId()).collect(Collectors.toSet()), (a, b) -> { Set c = new HashSet<>(a); @@ -124,6 +126,15 @@ private List getTagRankingVoList() { .collect(Collectors.toList()); } + private String resolveTagName(String tagName, List tagGroups) { + for (TagGroup tagGroup : tagGroups) { + if (tagGroup.contains(tagName)) { + return tagGroup.getName(); + } + } + return tagName; + } + private File toCsvFile(List tagRankingVoList) throws IOException { File file = File.createTempFile("tagRanking", "csv"); file.deleteOnExit(); diff --git a/module-batch/src/main/java/inspiration/infrastructure/google/GoogleApiConfig.java b/module-batch/src/main/java/inspiration/infrastructure/google/GoogleApiConfig.java new file mode 100644 index 0000000..5a28ef8 --- /dev/null +++ b/module-batch/src/main/java/inspiration/infrastructure/google/GoogleApiConfig.java @@ -0,0 +1,44 @@ +package inspiration.infrastructure.google; + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.support.HttpRequestWrapper; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.time.Duration; + +@Configuration +public class GoogleApiConfig { + @Value("${ygtang.google.api-key}") + private String googleApiKey; + + @Bean + public RestTemplate googleApiRestTemplate() { + return new RestTemplateBuilder() + .additionalInterceptors( + (request, body, execution) -> { + URI uri = UriComponentsBuilder.fromHttpRequest(request) + .queryParam("key", googleApiKey) + .build().toUri(); + + HttpRequest modifiedRequest = new HttpRequestWrapper(request) { + @NotNull + @Override + public URI getURI() { + return uri; + } + }; + return execution.execute(modifiedRequest, body); + } + ) + .setConnectTimeout(Duration.ofSeconds(1L)) + .setReadTimeout(Duration.ofSeconds(3L)) + .build(); + } +} diff --git a/module-batch/src/main/java/inspiration/infrastructure/google/GoogleSheetTagGroupService.java b/module-batch/src/main/java/inspiration/infrastructure/google/GoogleSheetTagGroupService.java new file mode 100644 index 0000000..890048c --- /dev/null +++ b/module-batch/src/main/java/inspiration/infrastructure/google/GoogleSheetTagGroupService.java @@ -0,0 +1,79 @@ +package inspiration.infrastructure.google; + +import inspiration.application.tag.TagGroup; +import inspiration.application.tag.TagGroupService; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@SuppressWarnings("ClassCanBeRecord") +public class GoogleSheetTagGroupService implements TagGroupService { + private final RestTemplate googleApiRestTemplate; + + @Value("${ygtang.google.sheet-id.tag-group}") + private String tagGroupSheetId; + + @Override + public List getTagGroups() { + ResponseEntity responseEntity = googleApiRestTemplate.getForEntity( + UriComponentsBuilder.newInstance() + .scheme("https") + .host("sheets.googleapis.com") + .path("/v4/spreadsheets/{spreadsheetId}") + .queryParam("ranges", "A1:J1000") + .queryParam("includeGridData", "true") + .build(tagGroupSheetId), + GoogleSheetResponse.class + ); + if (responseEntity.getBody() == null) { + throw new IllegalStateException("Failed to get tag groups from Google Sheet Api"); + } + return responseEntity.getBody().getSheets().get(0).getData().get(0).getRowData() + .stream() + .map(it -> TagGroup.from( + it.getValues() + .stream() + .map(ValuesResponse::getFormattedValue) + .filter(StringUtils::hasText) + .collect(Collectors.toList()) + ) + ) + .collect(Collectors.toList()); + } + + @Data + static class GoogleSheetResponse { + List sheets; + } + + @Data + static class SheetsResponse { + List data; + } + + @Data + static class DataResponse { + List rowData; + } + + @Data + static class RowDataResponse { + List values; + } + + @Data + static class ValuesResponse { + String formattedValue; + } +} + diff --git a/module-batch/src/main/resources/batch.yml b/module-batch/src/main/resources/batch.yml index 23220dd..2285e87 100644 --- a/module-batch/src/main/resources/batch.yml +++ b/module-batch/src/main/resources/batch.yml @@ -6,4 +6,8 @@ spring: ygtang: slack: token: - bot: ENC(/vYHx8EfqXQ5Q+wUpBxmEkde8Nz0i32a1I1uFPzi7X5bOsfQ1/BU7/enM9DMzHt2VO7FwgOELpKfAkjc98kzHG4stHPZKF3mJ5Xqf0p9SeAEyMvv+Y6JPZojjQ755zAT) \ No newline at end of file + bot: ENC(tiq6tuKYXrEiqutBkfkXJxKFiHBO7JlCgPDH8zAiU/C72XsYPXZuDZ+ZWAkx871dBAKF+befdkDbQZ1SF2eHVPa2pemI38jqmszaycYcic0hnzY9EVcDNMOmPTLJauPT) + google: + api-key: ENC(eSD4S2QU5YycRNsFnwcVBXfcjyhZZZGt752BAZEORv5WgbT60yy1VGDiKrpq6NYBbN0wVKNXSc78/OVrc2sr7X4anCsUjRh81mykpr9PWTM=) + sheet-id: + tag-group: ENC(W5y38wS4Qp1HatcqmlZqTRVcylum+99xzcLyKbf+UC/H4sdhpv8V9du2KfevbClTQBChW2nxo/6BerOK4Kp4rZv4Jfr9W+woHlIWyhPze4M=) \ No newline at end of file diff --git a/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryCustom.java b/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryCustom.java index c2bf843..07b4dd1 100644 --- a/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryCustom.java +++ b/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryCustom.java @@ -3,7 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.Collection; public interface InspirationRepositoryCustom { @@ -11,8 +11,8 @@ Page findDistinctByMemberIdAndTagIdInAndTypeAndCreatedDateTimeBetwe Long memberId, Collection tagIds, Collection inspirationTypes, - LocalDateTime createdDateTimeFrom, - LocalDateTime createdDateTimeTo, + LocalDate createdDateTimeFrom, + LocalDate createdDateTimeTo, Pageable pageable ); } diff --git a/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryImpl.java b/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryImpl.java index 8193bda..d92d798 100644 --- a/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryImpl.java +++ b/module-domain/src/main/java/inspiration/domain/inspiration/InspirationRepositoryImpl.java @@ -10,7 +10,8 @@ import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; import org.springframework.util.CollectionUtils; -import java.time.LocalDateTime; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.Collection; import java.util.Objects; @@ -27,8 +28,8 @@ public Page findDistinctByMemberIdAndTagIdInAndTypeAndCreatedDateTi Long memberId, Collection tagIds, Collection inspirationTypes, - LocalDateTime createdDateTimeFrom, - LocalDateTime createdDateTimeTo, + LocalDate createdDateTimeFrom, + LocalDate createdDateTimeTo, Pageable pageable ) { BooleanExpression expression = qInspiration.member.id.eq(memberId); @@ -39,10 +40,10 @@ public Page findDistinctByMemberIdAndTagIdInAndTypeAndCreatedDateTi expression = expression.and(qInspiration.type.in(inspirationTypes)); } if (createdDateTimeFrom != null) { - expression = expression.and(qInspiration.createdDateTime.goe(createdDateTimeFrom)); + expression = expression.and(qInspiration.createdDateTime.goe(createdDateTimeFrom.atStartOfDay())); } if (createdDateTimeTo != null) { - expression = expression.and(qInspiration.createdDateTime.lt(createdDateTimeTo)); + expression = expression.and(qInspiration.createdDateTime.loe(createdDateTimeTo.atTime(LocalTime.MAX))); } JPQLQuery query = from(qInspiration) diff --git a/module-domain/src/main/java/inspiration/infrastructure/spring/ExecutorConfig.java b/module-domain/src/main/java/inspiration/infrastructure/spring/ExecutorConfig.java new file mode 100644 index 0000000..673660d --- /dev/null +++ b/module-domain/src/main/java/inspiration/infrastructure/spring/ExecutorConfig.java @@ -0,0 +1,19 @@ +package inspiration.infrastructure.spring; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class ExecutorConfig { + @Bean + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize(10); + threadPoolTaskExecutor.setMaxPoolSize(10); + threadPoolTaskExecutor.setQueueCapacity(100); + threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); + threadPoolTaskExecutor.initialize(); + return threadPoolTaskExecutor; + } +} diff --git a/module-domain/src/main/resources/application.yml b/module-domain/src/main/resources/application.yml index 6734df8..1eeeabb 100644 --- a/module-domain/src/main/resources/application.yml +++ b/module-domain/src/main/resources/application.yml @@ -22,6 +22,8 @@ cloud: aws: region: static: ap-northeast-2 - + s3: + connection-timeout: 1000 + request-timeout: 3000 uri: webhook: diff --git a/module-domain/src/test/resources/application.yml b/module-domain/src/test/resources/application.yml index 79f3ac0..d7a6d1a 100644 --- a/module-domain/src/test/resources/application.yml +++ b/module-domain/src/test/resources/application.yml @@ -38,10 +38,6 @@ cloud: secret-key: region: static: - s3: - bucket: - stack: - auto: false logging: pattern: diff --git a/module-web/build.gradle b/module-web/build.gradle index 9f1ed88..6c24ceb 100644 --- a/module-web/build.gradle +++ b/module-web/build.gradle @@ -1,5 +1,3 @@ -apply plugin: "com.gorylenko.gradle-git-properties" - dependencies { implementation project(':module-domain') implementation project(':module-api') diff --git a/module-web/src/main/java/inspiration/infrastructure/WebConfig.java b/module-web/src/main/java/inspiration/infrastructure/WebConfig.java index 23e7ff5..f07829f 100644 --- a/module-web/src/main/java/inspiration/infrastructure/WebConfig.java +++ b/module-web/src/main/java/inspiration/infrastructure/WebConfig.java @@ -91,6 +91,7 @@ protected void addResourceHandlers(ResourceHandlerRegistry registry) { } @Override + @SuppressWarnings("unchecked") protected void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(createAuthenticationPrincipalArgumentResolver()); argumentResolvers.add(new PageableHandlerMethodArgumentResolver()); diff --git a/module-web/src/main/java/inspiration/v1/inspiration/InspirationController.java b/module-web/src/main/java/inspiration/v1/inspiration/InspirationController.java index fd1a043..baab004 100644 --- a/module-web/src/main/java/inspiration/v1/inspiration/InspirationController.java +++ b/module-web/src/main/java/inspiration/v1/inspiration/InspirationController.java @@ -26,7 +26,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.net.URI; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; @RestController @@ -111,8 +111,8 @@ public ResponseEntity inspirationTagging(HttpServletRequest http }) public ResponseEntity findInspirationByTag(Pageable pageable, @RequestBody List tagIds, @RequestParam(required = false) List types, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdDateTimeFrom, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createdDateTimeTo, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdDateTimeFrom, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate createdDateTimeTo, @ApiIgnore @AuthenticationPrincipal Long memberId) { Page inspirationResponsePage = inspirationService.findInspirationsByTags(tagIds, types, createdDateTimeFrom, createdDateTimeTo, memberId, pageable); diff --git a/module-web/src/main/resources/logback-spring.xml b/module-web/src/main/resources/logback-spring.xml index 9971073..a1666ea 100644 --- a/module-web/src/main/resources/logback-spring.xml +++ b/module-web/src/main/resources/logback-spring.xml @@ -1,8 +1,6 @@ - - @@ -22,13 +20,14 @@ + - + @@ -36,6 +35,7 @@ + diff --git a/module-web/src/main/resources/web.yml b/module-web/src/main/resources/web.yml index 90c4233..92b811a 100644 --- a/module-web/src/main/resources/web.yml +++ b/module-web/src/main/resources/web.yml @@ -18,4 +18,11 @@ management: endpoints: web: exposure: - include: "health,info" \ No newline at end of file + include: "health,info" + +logging: + level: + org: + apache: + catalina: + core: OFF diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..be4ad76 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,18 @@ +#!/bin/bash +APP_PATH=/home/ubuntu/app/inspiration +SCRIPT_PATH=/home/ubuntu/app/inspiration/scripts + +cp ${APP_PATH}/deploy/.env ${APP_PATH}/source/.env +export $(cat ${APP_PATH}/source/.env | grep -v ^# | xargs) >/dev/null + +cd ${SCRIPT_PATH} + +$SCRIPT_PATH/login_ecr.sh ${AWS_CLI_REGION} ${AWS_CLI_ACCOUNT_ID} + +docker stop ${ACTIVE} +sleep 3 + +docker-compose -f ${APP_PATH}/source/docker-compose.yml pull +sleep 1 +docker-compose -f ${APP_PATH}/source/docker-compose.yml up -d +sleep 10 diff --git a/scripts/health_check.sh b/scripts/health_check.sh index fe850a7..f937e9e 100644 --- a/scripts/health_check.sh +++ b/scripts/health_check.sh @@ -4,10 +4,11 @@ TARGET_URL=localhost APP_PATH=/home/ubuntu/app/inspiration +export $(cat ${APP_PATH}/source/.env | grep -v ^# | xargs) >/dev/null + echo "> Start health check of WAS at '${TARGET_URL}:${PORT}' ..." -for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10 -do +for RETRY_COUNT in 1 2 3 4 5 6 7 8 9 10; do echo "> #${RETRY_COUNT} trying..." RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${TARGET_URL}:"${PORT}"/health) diff --git a/scripts/login_ecr.sh b/scripts/login_ecr.sh new file mode 100644 index 0000000..300f921 --- /dev/null +++ b/scripts/login_ecr.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +REGION=$1 +AWS_ACCOUNT_ID=$2 + +aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com