From 8cb6e2feff7f2297c5bb3222575277d3fb4c83be Mon Sep 17 00:00:00 2001 From: Junho Hwang <72647031+juno-junho@users.noreply.github.com> Date: Fri, 17 May 2024 23:07:43 +0900 Subject: [PATCH] =?UTF-8?q?csv=20=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20dependency=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 16 ++++-- .../global/advice/GlobalExceptionHandler.java | 2 +- .../profanity/BadWordExceptionMessage.java | 17 ------ .../profanity/ProfanityCheckValidator.java | 2 +- .../profanity/ProfanityExceptionMessage.java | 22 ++++++++ .../annotation/profanity/ProfanityLoader.java | 2 +- .../spaceclub/global/aws/S3Properties.java | 3 +- .../global/aws/s3/FileNameCreator.java | 7 ++- .../global/aws/s3/S3FileUploader.java | 52 +++++++++++++++++++ .../com/spaceclub/global/aws/s3/S3Folder.java | 4 +- .../global/aws/s3/S3ImageUploader.java | 4 +- .../global/config/ProfanityConfig.java | 5 +- src/main/resources/application-develop.yml | 6 ++- src/main/resources/application-local.yml | 6 ++- .../global/s3/FileNameCreatorTest.java | 12 ++--- src/test/resources/application-test.yml | 3 +- src/test/resources/application.yml | 3 ++ 17 files changed, 127 insertions(+), 39 deletions(-) delete mode 100644 src/main/java/com/spaceclub/global/annotation/profanity/BadWordExceptionMessage.java create mode 100644 src/main/java/com/spaceclub/global/annotation/profanity/ProfanityExceptionMessage.java create mode 100644 src/main/java/com/spaceclub/global/aws/s3/S3FileUploader.java diff --git a/build.gradle b/build.gradle index 3a892f05..9c158ab8 100644 --- a/build.gradle +++ b/build.gradle @@ -43,23 +43,31 @@ dependencies { // concurrent-trees for trie implementation 'com.googlecode.concurrent-trees:concurrent-trees:2.6.1' + // apache commons csv for csv generating csv file + implementation 'org.apache.commons:commons-csv:1.11.0' + + // springboot implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + // mail + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // database runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.security:spring-security-test' - // mail - implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - // monitoring implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/src/main/java/com/spaceclub/global/advice/GlobalExceptionHandler.java b/src/main/java/com/spaceclub/global/advice/GlobalExceptionHandler.java index eb1bb639..c74aa89e 100644 --- a/src/main/java/com/spaceclub/global/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/spaceclub/global/advice/GlobalExceptionHandler.java @@ -17,7 +17,7 @@ import java.util.List; -import static com.spaceclub.global.annotation.profanity.BadWordExceptionMessage.BAD_WORD_DETECTED; +import static com.spaceclub.global.annotation.profanity.ProfanityExceptionMessage.BAD_WORD_DETECTED; import static com.spaceclub.global.exception.GlobalExceptionCode.INVALID_REQUEST; import static com.spaceclub.global.exception.GlobalExceptionCode.MAX_IMAGE_SIZE_EXCEEDED; import static org.springframework.http.HttpStatus.BAD_REQUEST; diff --git a/src/main/java/com/spaceclub/global/annotation/profanity/BadWordExceptionMessage.java b/src/main/java/com/spaceclub/global/annotation/profanity/BadWordExceptionMessage.java deleted file mode 100644 index 5f02d8de..00000000 --- a/src/main/java/com/spaceclub/global/annotation/profanity/BadWordExceptionMessage.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.spaceclub.global.annotation.profanity; - -import lombok.Getter; - -@Getter -public enum BadWordExceptionMessage { - - FAIL_BAD_WORD_SETUP("비속어 목록 Trie 생성 실패"), - BAD_WORD_DETECTED("비속어가 발견 되었습니다"); - - private final String message; - - BadWordExceptionMessage(String message) { - this.message = message; - } - -} diff --git a/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityCheckValidator.java b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityCheckValidator.java index f3d2485a..9838cddd 100644 --- a/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityCheckValidator.java +++ b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityCheckValidator.java @@ -5,7 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import static com.spaceclub.global.annotation.profanity.BadWordExceptionMessage.BAD_WORD_DETECTED; +import static com.spaceclub.global.annotation.profanity.ProfanityExceptionMessage.BAD_WORD_DETECTED; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityExceptionMessage.java b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityExceptionMessage.java new file mode 100644 index 00000000..5b3e9d37 --- /dev/null +++ b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityExceptionMessage.java @@ -0,0 +1,22 @@ +package com.spaceclub.global.annotation.profanity; + +import lombok.Getter; + +@Getter +public enum ProfanityExceptionMessage { + + FAIL_BAD_WORD_SETUP("비속어 목록 Trie 생성 실패"), + BAD_WORD_DETECTED("비속어가 발견 되었습니다"), + INVALID_EXTENSION("md,txt 파일만 업로드 가능합니다."), + FAILED_TO_SAVE("금칙어 저장에 실패하였습니다."), + BAD_WORD_ALREADY_EXISTS("이미 존재하는 금칙어입니다."), + FAILED_TO_CREATE_CSV("CSV 파일 생성에 실패하였습니다.") + ; + + private final String message; + + ProfanityExceptionMessage(String message) { + this.message = message; + } + +} diff --git a/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityLoader.java b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityLoader.java index 0fdf3dde..346f85c0 100644 --- a/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityLoader.java +++ b/src/main/java/com/spaceclub/global/annotation/profanity/ProfanityLoader.java @@ -17,7 +17,7 @@ import java.util.Spliterator; import java.util.stream.StreamSupport; -import static com.spaceclub.global.annotation.profanity.BadWordExceptionMessage.FAIL_BAD_WORD_SETUP; +import static com.spaceclub.global.annotation.profanity.ProfanityExceptionMessage.FAIL_BAD_WORD_SETUP; @Slf4j @Component diff --git a/src/main/java/com/spaceclub/global/aws/S3Properties.java b/src/main/java/com/spaceclub/global/aws/S3Properties.java index 30820a9f..5f9b2dfc 100644 --- a/src/main/java/com/spaceclub/global/aws/S3Properties.java +++ b/src/main/java/com/spaceclub/global/aws/S3Properties.java @@ -6,7 +6,8 @@ @ConfigurationProperties(prefix = "s3") public record S3Properties( - String bucket, + String imageBucket, + String fileBucket, String region, String url, List validExtensions diff --git a/src/main/java/com/spaceclub/global/aws/s3/FileNameCreator.java b/src/main/java/com/spaceclub/global/aws/s3/FileNameCreator.java index f7d85732..916f2229 100644 --- a/src/main/java/com/spaceclub/global/aws/s3/FileNameCreator.java +++ b/src/main/java/com/spaceclub/global/aws/s3/FileNameCreator.java @@ -10,11 +10,12 @@ public class FileNameCreator { private static final String DOT = "."; + private static final String CSV_EXTENSION = "csv"; private static final String FILE_FORMAT = "%s/%s_%s.%s"; private final S3Properties s3Properties; - public String createFileName(S3Folder folder, String originalName, String timestamp) { + public String createImageFileKey(S3Folder folder, String originalName, String timestamp) { int lastDot = originalName.lastIndexOf(DOT); String fileName = originalName.substring(0, lastDot); String fileExtension = originalName.substring(lastDot + 1); @@ -23,6 +24,10 @@ public String createFileName(S3Folder folder, String originalName, String timest return String.format(FILE_FORMAT, folder.getFolder(), fileName, timestamp, fileExtension); } + public String createCSVFileKey(S3Folder folder, String fileName, String timestamp) { + return String.format(FILE_FORMAT, folder.getFolder(), fileName, timestamp, CSV_EXTENSION); + } + private void validateFileExtension(String fileExtension) { boolean invalidExtension = s3Properties.validExtensions() .stream() diff --git a/src/main/java/com/spaceclub/global/aws/s3/S3FileUploader.java b/src/main/java/com/spaceclub/global/aws/s3/S3FileUploader.java new file mode 100644 index 00000000..76731697 --- /dev/null +++ b/src/main/java/com/spaceclub/global/aws/s3/S3FileUploader.java @@ -0,0 +1,52 @@ +package com.spaceclub.global.aws.s3; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.spaceclub.global.aws.S3Properties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static com.spaceclub.global.exception.GlobalExceptionCode.FAIL_FILE_UPLOAD; + +@Component +@RequiredArgsConstructor +public class S3FileUploader { + + private static final String DATE_FORMAT = "yyyyMMdd_HHmmss"; + private static final String CSV_FILE_NAME = "profanity_info"; + + private final AmazonS3 amazonS3; + private final S3Properties s3Properties; + + public String uploadProfanityInfo(String content) { + final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT)); + String csvFileKey = new FileNameCreator(s3Properties).createCSVFileKey(S3Folder.PROFANITY_INFO, CSV_FILE_NAME, timestamp); + ObjectMetadata objectMetaData = createMetaData(content); + + try (InputStream inputStream = new ByteArrayInputStream(content.getBytes())) { + final PutObjectRequest request = new PutObjectRequest(s3Properties.fileBucket(), csvFileKey, inputStream, objectMetaData) + .withCannedAcl(CannedAccessControlList.PublicRead); + amazonS3.putObject(request); + + return s3Properties.url() + csvFileKey; + } catch (IOException e) { + throw new IllegalStateException(FAIL_FILE_UPLOAD.toString()); + } + } + + private ObjectMetadata createMetaData(String content) { + ObjectMetadata objectMetaData = new ObjectMetadata(); + objectMetaData.setContentType("text/csv"); + objectMetaData.setContentLength(content.length()); + return objectMetaData; + } + +} diff --git a/src/main/java/com/spaceclub/global/aws/s3/S3Folder.java b/src/main/java/com/spaceclub/global/aws/s3/S3Folder.java index b5a466e8..f1296f96 100644 --- a/src/main/java/com/spaceclub/global/aws/s3/S3Folder.java +++ b/src/main/java/com/spaceclub/global/aws/s3/S3Folder.java @@ -9,7 +9,9 @@ public enum S3Folder { LOGO("club-logo"), COVER("club-cover"), USER_PROFILE("user-profile-image"), - POST_IMAGE("post-image"); + POST_IMAGE("post-image"), + PROFANITY_INFO("profanity"), + ; private final String folder; diff --git a/src/main/java/com/spaceclub/global/aws/s3/S3ImageUploader.java b/src/main/java/com/spaceclub/global/aws/s3/S3ImageUploader.java index 7c314b61..4e9f52d3 100644 --- a/src/main/java/com/spaceclub/global/aws/s3/S3ImageUploader.java +++ b/src/main/java/com/spaceclub/global/aws/s3/S3ImageUploader.java @@ -30,12 +30,12 @@ public String upload(MultipartFile image, S3Folder folder) { if (originalFilename == null) throw new IllegalArgumentException(FAIL_FILE_UPLOAD.toString()); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_FORMAT)); - String fileName = new FileNameCreator(s3Properties).createFileName(folder, originalFilename, timestamp); + String fileName = new FileNameCreator(s3Properties).createImageFileKey(folder, originalFilename, timestamp); ObjectMetadata objectMetaData = createMetaData(image); try (InputStream inputStream = image.getInputStream()) { amazonS3Client.putObject( - new PutObjectRequest(s3Properties.bucket(), fileName, inputStream, objectMetaData) + new PutObjectRequest(s3Properties.imageBucket(), fileName, inputStream, objectMetaData) .withCannedAcl(CannedAccessControlList.PublicRead)); } catch (IOException e) { throw new IllegalStateException(FAIL_FILE_UPLOAD.toString()); diff --git a/src/main/java/com/spaceclub/global/config/ProfanityConfig.java b/src/main/java/com/spaceclub/global/config/ProfanityConfig.java index 75c98425..b2890fce 100644 --- a/src/main/java/com/spaceclub/global/config/ProfanityConfig.java +++ b/src/main/java/com/spaceclub/global/config/ProfanityConfig.java @@ -2,9 +2,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.List; + @ConfigurationProperties(prefix = "profanity") public record ProfanityConfig( - String filePath + String filePath, + List validExtensions ) { } diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 26feb9c2..99a42f62 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -26,7 +26,8 @@ oauth: admin-key-prefix: KakaoAK s3: - bucket: ENC(JoDpjsUcxIviI7KX5vVl47Ey3PAFn6GffGgyuKeWoek=) + file-bucket: ENC(62oqQc22U4+n1L8xyyRqtb10UWqpPwChIQ7WHe6gJ3s=) + image-bucket: ENC(JoDpjsUcxIviI7KX5vVl47Ey3PAFn6GffGgyuKeWoek=) region: ENC(906H8BMOAumiA+H0nevzxJUoUGzBG2Nh) url: ENC(iMleKm/uvFwQ7k0EGbj94AFJkend3u7iqYYb8QxSNMPU/nCmbB1kXG/PdovxHoc41yeIHkhSwjfjGSyp5DlMmHEqib5VWr6LkptpQ9KX9Hs=) @@ -46,3 +47,6 @@ web: profanity: file-path: /home/ubuntu/bad_word_list.txt + valid-extensions: + - txt + - md diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index b3b113de..a6c3111b 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -34,7 +34,8 @@ oauth: admin-key-prefix: KakaoAK s3: - bucket: ENC(JoDpjsUcxIviI7KX5vVl47Ey3PAFn6GffGgyuKeWoek=) + image-bucket: ENC(JoDpjsUcxIviI7KX5vVl47Ey3PAFn6GffGgyuKeWoek=) + file-bucket: ENC(62oqQc22U4+n1L8xyyRqtb10UWqpPwChIQ7WHe6gJ3s=) region: ENC(906H8BMOAumiA+H0nevzxJUoUGzBG2Nh) url: ENC(iMleKm/uvFwQ7k0EGbj94AFJkend3u7iqYYb8QxSNMPU/nCmbB1kXG/PdovxHoc41yeIHkhSwjfjGSyp5DlMmHEqib5VWr6LkptpQ9KX9Hs=) @@ -51,3 +52,6 @@ web: profanity: file-path: src/main/resources/secrets/bad_word_list.txt + valid-extensions: + - txt + - md diff --git a/src/test/java/com/spaceclub/global/s3/FileNameCreatorTest.java b/src/test/java/com/spaceclub/global/s3/FileNameCreatorTest.java index cb91ec64..f7cfa4fe 100644 --- a/src/test/java/com/spaceclub/global/s3/FileNameCreatorTest.java +++ b/src/test/java/com/spaceclub/global/s3/FileNameCreatorTest.java @@ -23,7 +23,7 @@ class FileNameCreatorTest { @Test void 폴더명_파일명_타임스탬프_확장자_형식으로_파일명을_생성한다() { // given - S3Properties s3Properties = new S3Properties("s3bucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); + S3Properties s3Properties = new S3Properties("s3ImageBucket", "s3FileBucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); FileNameCreator fileNameCreator = new FileNameCreator(s3Properties); S3Folder folder = S3Folder.COVER; String folderName = folder.getFolder(); @@ -31,7 +31,7 @@ class FileNameCreatorTest { String timestamp = "202404191111"; // when - String fileName = fileNameCreator.createFileName(folder, originalFileName, timestamp); + String fileName = fileNameCreator.createImageFileKey(folder, originalFileName, timestamp); // then assertThat(fileName).isEqualTo(folderName + "/originalName_202404191111.jpg"); @@ -40,14 +40,14 @@ class FileNameCreatorTest { @Test void 확장자가_jpg_jpeg_png_가_아니면_예외를_생성한다() { // given - S3Properties s3Properties = new S3Properties("s3bucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); + S3Properties s3Properties = new S3Properties("s3ImageBucket", "s3FileBucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); FileNameCreator fileNameCreator = new FileNameCreator(s3Properties); S3Folder folder = S3Folder.COVER; String originalFileName = "originalName.gif"; String timestamp = "202404191111"; // when, then - assertThatThrownBy(() -> fileNameCreator.createFileName(folder, originalFileName, timestamp)) + assertThatThrownBy(() -> fileNameCreator.createImageFileKey(folder, originalFileName, timestamp)) .isInstanceOf(MultipartException.class) .hasMessage(INVALID_FILE_EXTENSION.toString()); } @@ -56,7 +56,7 @@ class FileNameCreatorTest { @ValueSource(strings = {"jpg", "jpeg", "png"}) void 확장자가_jpg_jpeg_png이면_예외를_발생하지_않는다(String extension) { // given - S3Properties s3Properties = new S3Properties("s3bucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); + S3Properties s3Properties = new S3Properties("s3ImageBucket", "s3FileBucket", "s3region", "s3url", List.of("jpg", "jpeg", "png")); FileNameCreator fileNameCreator = new FileNameCreator(s3Properties); S3Folder folder = S3Folder.COVER; String originalFileName = "originalName." + extension; @@ -64,7 +64,7 @@ class FileNameCreatorTest { // when, then assertThatNoException() - .isThrownBy(() -> fileNameCreator.createFileName(folder, originalFileName, timestamp)); + .isThrownBy(() -> fileNameCreator.createImageFileKey(folder, originalFileName, timestamp)); } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index b3bf5c94..bfa7c3e7 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -19,7 +19,8 @@ spring: defer-datasource-initialization: true s3: - bucket: space-club-image-bucket + file-bucket: file-bucket-test + image-bucket: image-bucket-test region: region url: https://test.com/ diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 4536c310..89d7ca00 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -89,6 +89,9 @@ web: profanity: file-path: src/main/resources/secrets/bad_word_list.txt + valid-extensions: + - txt + - md invite: link-prefix: "https://space-club.site/api/v1/clubs/invite/"