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