diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ce5b24a9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Jaeyeop-Jung @Pull-Stack @kdomo \ No newline at end of file diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 68% rename from pull_request_template.md rename to .github/pull_request_template.md index f7317f15..88b7bd54 100644 --- a/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,11 @@ ## 관련 이슈 번호 + + ## 설명 ## 변경사항 + diff --git a/.gitignore b/.gitignore index e48b6be6..5d47a5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,8 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ \ No newline at end of file +.vscode/ + +### Custom ### +logs/ +*.pem \ No newline at end of file diff --git a/build.gradle b/build.gradle index fc72faee..243d1272 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'io.spring.dependency-management' version '1.0.15.RELEASE' } -group = 'com.recodeit' +group = 'com.recordit' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' @@ -12,6 +12,10 @@ configurations { compileOnly { extendsFrom annotationProcessor } + all { + // log4j2를 위해 logback 의존성 제거 + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } } repositories { @@ -24,14 +28,39 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.session:spring-session-data-redis' + implementation 'io.springfox:springfox-boot-starter:3.0.0' + implementation 'io.springfox:springfox-swagger-ui:3.0.0' + + // embedded redis + implementation('it.ozimov:embedded-redis:0.7.3') { + exclude group: "org.slf4j", module: "slf4j-simple" + } + compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // use static mock + testImplementation 'org.mockito:mockito-inline:4.5.1' + + // use ConfigurationProperties + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + // use s3 + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.372' + + // log4j2 + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + } tasks.named('test') { useJUnitPlatform() } + +jar { + enabled = false +} \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 00000000..6428fde0 --- /dev/null +++ b/dockerfile @@ -0,0 +1,4 @@ +FROM adoptopenjdk/openjdk11 +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file diff --git a/naver-checkstyle-rules.xml b/naver-checkstyle-rules.xml new file mode 100644 index 00000000..5eefb788 --- /dev/null +++ b/naver-checkstyle-rules.xml @@ -0,0 +1,439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/naver-checkstyle-suppressions.xml b/naver-checkstyle-suppressions.xml new file mode 100644 index 00000000..efd97d80 --- /dev/null +++ b/naver-checkstyle-suppressions.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/naver-intellij-formatter.xml b/naver-intellij-formatter.xml new file mode 100644 index 00000000..a4f6cc7d --- /dev/null +++ b/naver-intellij-formatter.xml @@ -0,0 +1,62 @@ + + + \ No newline at end of file diff --git a/schema/category.sql b/schema/category.sql new file mode 100644 index 00000000..260ac2aa --- /dev/null +++ b/schema/category.sql @@ -0,0 +1,22 @@ +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('축하 레코드', now(), now(), null); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('위로 레코드', now(), now(), null); + +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('축하해주세요', now(), now(), 1); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('기념일이에요', now(), now(), 1); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('연애중이에요', now(), now(), 1); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('행복해요', now(), now(), 1); + +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('위로해주세요', now(), now(), 2); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('공감이 필요해요', now(), now(), 2); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('내편이 되어주세요', now(), now(), 2); +insert into RECORD_CATEGORY(NAME, CREATED_AT, MODIFIED_AT, PARENT_RECORD_CATEGORY_ID) + value ('우울해요', now(), now(), 2); \ No newline at end of file diff --git a/schema/color.sql b/schema/color.sql new file mode 100644 index 00000000..e1c269d6 --- /dev/null +++ b/schema/color.sql @@ -0,0 +1,10 @@ +INSERT INTO RECORD_COLOR(RECORD_COLOR_ID, NAME, HEX_CODE, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'icon-purple', '#9067E5', now(), now(), null); +INSERT INTO RECORD_COLOR(RECORD_COLOR_ID, NAME, HEX_CODE, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'icon-yellow', '#F3D06C', now(), now(), null); +INSERT INTO RECORD_COLOR(RECORD_COLOR_ID, NAME, HEX_CODE, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'icon-pink', '#D78A86', now(), now(), null); +INSERT INTO RECORD_COLOR(RECORD_COLOR_ID, NAME, HEX_CODE, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'icon-blue', '#6F99F2', now(), now(), null); +INSERT INTO RECORD_COLOR(RECORD_COLOR_ID, NAME, HEX_CODE, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'icon-green', '#78BCB7', now(), now(), null); \ No newline at end of file diff --git a/schema/icon.sql b/schema/icon.sql new file mode 100644 index 00000000..dd858fcc --- /dev/null +++ b/schema/icon.sql @@ -0,0 +1,26 @@ +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'crown', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'gift', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'heart', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'like', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'lock', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'modal', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'moon', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'music', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'rocket', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'speechbubble', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'trashcan', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'umbrella', now(), now(), null); +INSERT INTO RECORD_ICON(RECORD_ICON_ID, NAME, CREATED_AT, MODIFIED_AT, DELETED_AT) +VALUES (null, 'wine', now(), now(), null); \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 86a18c1d..e6d960dd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'recodeIt-server' +rootProject.name = 'recordIt-server' diff --git a/src/main/java/com/recodeit/server/RecodeItServerApplication.java b/src/main/java/com/recodeit/server/RecodeItServerApplication.java deleted file mode 100644 index 2d4cbae3..00000000 --- a/src/main/java/com/recodeit/server/RecodeItServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.recodeit.server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class RecodeItServerApplication { - - public static void main(String[] args) { - SpringApplication.run(RecodeItServerApplication.class, args); - } - -} diff --git a/src/main/java/com/recordit/server/RecordItServerApplication.java b/src/main/java/com/recordit/server/RecordItServerApplication.java new file mode 100644 index 00000000..fc245572 --- /dev/null +++ b/src/main/java/com/recordit/server/RecordItServerApplication.java @@ -0,0 +1,21 @@ +package com.recordit.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@SpringBootApplication +@EnableConfigurationProperties +@ConfigurationPropertiesScan +@EnableJpaAuditing +@EnableCaching +public class RecordItServerApplication { + + public static void main(String[] args) { + SpringApplication.run(RecordItServerApplication.class, args); + } + +} diff --git a/src/main/java/com/recordit/server/configuration/CacheConfiguration.java b/src/main/java/com/recordit/server/configuration/CacheConfiguration.java new file mode 100644 index 00000000..44af4e68 --- /dev/null +++ b/src/main/java/com/recordit/server/configuration/CacheConfiguration.java @@ -0,0 +1,40 @@ +package com.recordit.server.configuration; + +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class CacheConfiguration { + + @Bean + public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig() + .disableCachingNullValues() + .prefixCacheNameWith("cache:") + .serializeKeysWith( + RedisSerializationContext + .SerializationPair + .fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext + .SerializationPair + .fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)) + ); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(configuration) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/recordit/server/configuration/RedisConfiguration.java b/src/main/java/com/recordit/server/configuration/RedisConfiguration.java new file mode 100644 index 00000000..5401acac --- /dev/null +++ b/src/main/java/com/recordit/server/configuration/RedisConfiguration.java @@ -0,0 +1,53 @@ +package com.recordit.server.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfiguration { + + private final String host; + private final int port; + private final String password; + + public RedisConfiguration( + @Value("${spring.redis.host}") String host, + @Value("${spring.redis.port}") int port, + @Value("${spring.redis.password}") String password + ) { + this.host = host; + this.port = port; + this.password = password; + } + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(host); + configuration.setPort(port); + configuration.setPassword(password); + return new LettuceConnectionFactory(configuration); + } + + @Bean + public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + StringRedisTemplate redisTemplate = new StringRedisTemplate(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } + + @Bean + public RedisSerializer springSessionDefaultRedisSerializer() { + return new Jackson2JsonRedisSerializer(Object.class); + } +} diff --git a/src/main/java/com/recordit/server/configuration/S3Configuration.java b/src/main/java/com/recordit/server/configuration/S3Configuration.java new file mode 100644 index 00000000..b13a2b32 --- /dev/null +++ b/src/main/java/com/recordit/server/configuration/S3Configuration.java @@ -0,0 +1,34 @@ +package com.recordit.server.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.recordit.server.environment.S3Properties; + +import lombok.RequiredArgsConstructor; + +@Configuration +@RequiredArgsConstructor +public class S3Configuration { + + private final S3Properties s3Properties; + + @Bean + public AmazonS3 amazonS3() { + AWSCredentials awsCredentials = new BasicAWSCredentials( + s3Properties.getCredentials().getAccessKey(), + s3Properties.getCredentials().getSecretKey() + ); + + return AmazonS3ClientBuilder + .standard() + .withRegion(s3Properties.getRegion()) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/com/recordit/server/configuration/SwaggerConfiguration.java b/src/main/java/com/recordit/server/configuration/SwaggerConfiguration.java new file mode 100644 index 00000000..0910f210 --- /dev/null +++ b/src/main/java/com/recordit/server/configuration/SwaggerConfiguration.java @@ -0,0 +1,32 @@ +package com.recordit.server.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerConfiguration { + @Bean + public Docket api() { + return new Docket(DocumentationType.SWAGGER_2) + .useDefaultResponseMessages(false) + .select() + .apis(RequestHandlerSelectors.basePackage("com.recordit")) + .paths(PathSelectors.any()) + .build().apiInfo(apiInfo()); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("RecordIt Backend - DEV") + .description("RecordIt Backend - DEV 서버 API 명세") + .version("1.0") + .build(); + } +} diff --git a/src/main/java/com/recordit/server/configuration/WebMvcConfiguration.java b/src/main/java/com/recordit/server/configuration/WebMvcConfiguration.java new file mode 100644 index 00000000..bfefdde1 --- /dev/null +++ b/src/main/java/com/recordit/server/configuration/WebMvcConfiguration.java @@ -0,0 +1,39 @@ +package com.recordit.server.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.recordit.server.converter.LoginTypeConverter; + +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + private final long MAX_AGE_SECS = 3000; + + private final String originPattern; + + public WebMvcConfiguration( + @Value("${cors.origin}") String originPattern + ) { + this.originPattern = originPattern; + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns(originPattern) + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true) + .exposedHeaders("Set-Cookie") + .maxAge(MAX_AGE_SECS); + } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new LoginTypeConverter()); + } +} diff --git a/src/main/java/com/recordit/server/constant/ImageFileExtension.java b/src/main/java/com/recordit/server/constant/ImageFileExtension.java new file mode 100644 index 00000000..00858972 --- /dev/null +++ b/src/main/java/com/recordit/server/constant/ImageFileExtension.java @@ -0,0 +1,7 @@ +package com.recordit.server.constant; + +public enum ImageFileExtension { + JPG, JPEG, jpg, jpeg, + PNG, png, + SVG, svg +} diff --git a/src/main/java/com/recordit/server/constant/LoginType.java b/src/main/java/com/recordit/server/constant/LoginType.java new file mode 100644 index 00000000..45b8d908 --- /dev/null +++ b/src/main/java/com/recordit/server/constant/LoginType.java @@ -0,0 +1,18 @@ +package com.recordit.server.constant; + +import java.util.Arrays; + +import com.recordit.server.exception.member.NotMatchLoginTypeException; + +public enum LoginType { + LOCAL, + KAKAO, + GOOGLE; + + public static LoginType findByString(String str) { + return Arrays.stream(LoginType.values()) + .filter(loginType -> loginType.name().equals(str)) + .findFirst() + .orElseThrow(() -> new NotMatchLoginTypeException("일치하는 로그인 타입이 없습니다.")); + } +} diff --git a/src/main/java/com/recordit/server/constant/OauthConstants.java b/src/main/java/com/recordit/server/constant/OauthConstants.java new file mode 100644 index 00000000..cbe3024c --- /dev/null +++ b/src/main/java/com/recordit/server/constant/OauthConstants.java @@ -0,0 +1,25 @@ +package com.recordit.server.constant; + +public enum OauthConstants { + CLIENT_ID("client_id"), + CLIENT_SECRET("client_secret"), + CODE("code"), + GRANT_TYPE("grant_type"), + REDIRECT_URI("redirect_uri"), + ID_TOKEN("id_token"), + AUTHORIZATION("Authorization"); + + public final String key; + + OauthConstants(String key) { + this.key = key; + } + + public static String getFixGrantType() { + return "authorization_code"; + } + + public static String getFixPrefixJwt() { + return "Bearer "; + } +} diff --git a/src/main/java/com/recordit/server/constant/RefType.java b/src/main/java/com/recordit/server/constant/RefType.java new file mode 100644 index 00000000..3e9ff8c0 --- /dev/null +++ b/src/main/java/com/recordit/server/constant/RefType.java @@ -0,0 +1,5 @@ +package com.recordit.server.constant; + +public enum RefType { + RECORD, COMMENT +} diff --git a/src/main/java/com/recordit/server/constant/RegisterSessionConstants.java b/src/main/java/com/recordit/server/constant/RegisterSessionConstants.java new file mode 100644 index 00000000..ad4c70b1 --- /dev/null +++ b/src/main/java/com/recordit/server/constant/RegisterSessionConstants.java @@ -0,0 +1,6 @@ +package com.recordit.server.constant; + +public class RegisterSessionConstants { + public static final String PREFIX_REGISTER_SESSION = "REGISTER_SESSION="; + public static final long TIMEOUT = 30; +} diff --git a/src/main/java/com/recordit/server/controller/CommentController.java b/src/main/java/com/recordit/server/controller/CommentController.java new file mode 100644 index 00000000..30ecf00e --- /dev/null +++ b/src/main/java/com/recordit/server/controller/CommentController.java @@ -0,0 +1,77 @@ +package com.recordit.server.controller; + +import javax.validation.Valid; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.dto.comment.CommentRequestDto; +import com.recordit.server.dto.comment.CommentResponseDto; +import com.recordit.server.dto.comment.WriteCommentRequestDto; +import com.recordit.server.dto.comment.WriteCommentResponseDto; +import com.recordit.server.exception.ErrorMessage; +import com.recordit.server.service.CommentService; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/comment") +public class CommentController { + + private final CommentService commentService; + + @ApiOperation( + value = "레코드에 댓글 작성", + notes = "레코드에 댓글을 작성합니다" + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "API 정상 작동", response = WriteCommentResponseDto.class + ), + @ApiResponse( + code = 400, message = "잘못된 요청", response = ErrorMessage.class + ) + }) + @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity writeComment( + @ApiParam(required = true) @RequestPart(required = true) @Valid WriteCommentRequestDto writeCommentRequestDto, + @ApiParam @RequestPart(required = false) MultipartFile attachment + ) { + return ResponseEntity.ok(commentService.writeComment(writeCommentRequestDto, attachment)); + } + + @ApiOperation( + value = "레코드의 댓글을 조회", + notes = "레코드의 댓글을 조회합니다\t\n\t\n" + + "레코드 ID는 필수 지정해야 합니다.\t\n" + + "부모 댓글 ID의 경우 지정하지 않으면 레코드의 Depth가 0인 댓글들을 조회하고, " + + "ID를 지정하면 해당 부모의 대댓글인 Depth가 1인 댓글들을 조회합니다.\t\n\t\n" + + "댓글 조회의 정렬 기준은 댓글 생성 시간으로 오름차순입니다." + ) + @ApiResponses({ + @ApiResponse( + code = 200, + message = "API 정상 작동 / 댓글 조회 완료", response = CommentResponseDto.class + ), + @ApiResponse( + code = 400, message = "잘못된 요청", + response = ErrorMessage.class + ) + }) + @GetMapping + public ResponseEntity getComment(@Valid @ModelAttribute CommentRequestDto commentRequestDto) { + return ResponseEntity.ok(commentService.getCommentsBy(commentRequestDto)); + } +} diff --git a/src/main/java/com/recordit/server/controller/MemberController.java b/src/main/java/com/recordit/server/controller/MemberController.java new file mode 100644 index 00000000..fa81b0e9 --- /dev/null +++ b/src/main/java/com/recordit/server/controller/MemberController.java @@ -0,0 +1,135 @@ +package com.recordit.server.controller; + +import java.util.Optional; + +import javax.validation.Valid; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.dto.member.LoginRequestDto; +import com.recordit.server.dto.member.RegisterRequestDto; +import com.recordit.server.dto.member.RegisterSessionResponseDto; +import com.recordit.server.exception.member.DuplicateNicknameException; +import com.recordit.server.service.MemberService; +import com.recordit.server.util.SessionUtil; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.ResponseHeader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/member") +@Slf4j +public class MemberController { + + private final MemberService memberService; + private final SessionUtil sessionUtil; + + @ApiOperation( + value = "Oauth 로그인", + notes = "로그인 타입과 Oauth 토큰을 통해 Oauth 로그인을 진행합니다." + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "API 정상 작동 / Header에 세션 응답", + responseHeaders = { + @ResponseHeader(name = "Set-cookie: SESSION=FOO;", description = "FOO = 서버의 세션", response = String.class) + } + ), + @ApiResponse( + code = 400, message = "API에서 지정한 LoginType이 아닐 경우입니다" + ), + @ApiResponse( + code = 401, message = "회원정보가 없어 회원가입이 필요한 경우입니다\t\n" + + "Body로 응답된 register_session과 사용자에게 닉네임을 받아 '/member/oauth/register/{loginType}'으로 요청하세요", + response = RegisterSessionResponseDto.class + ) + }) + @PostMapping("/oauth/login/{loginType}") + public ResponseEntity oauthLogin( + @ApiParam(allowableValues = "KAKAO, GOOGLE", required = true) @PathVariable("loginType") LoginType loginType, + @RequestBody LoginRequestDto loginRequestDto + ) { + + Optional registerSessionResponseDto = memberService.oauthLogin( + loginType, + loginRequestDto + ); + if (registerSessionResponseDto.isEmpty()) { + return ResponseEntity.ok().build(); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(registerSessionResponseDto.get()); + } + + @ApiOperation( + value = "회원가입", + notes = "로그인 타입, register_session, 닉네임을 받아 회원가입을 진행합니다" + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "API 정상 작동 / Header에 세션 응답", + responseHeaders = { + @ResponseHeader(name = "Set-cookie: SESSION=FOO;", description = "FOO = 서버의 세션", response = String.class) + } + ), + @ApiResponse( + code = 400, message = "API에서 지정한 LoginType이 아닐 경우입니다" + ), + @ApiResponse(code = 428, message = "register_session 정보가 Redis에 없거나 비정상적일 경우"), + @ApiResponse(code = 409, message = "닉네임이 중복 된 경우") + }) + @PostMapping("/oauth/register/{loginType}") + public ResponseEntity oauthRegister( + @ApiParam(allowableValues = "KAKAO, GOOGLE", required = true) @PathVariable("loginType") LoginType loginType, + @RequestBody @Valid RegisterRequestDto registerRequestDto + ) { + memberService.oauthRegister(loginType, registerRequestDto); + return new ResponseEntity(HttpStatus.OK); + } + + @ApiOperation( + value = "닉네임 중복확인", + notes = "닉네임을 받아 해당 닉네임이 중복되었는지 판별" + ) + @ApiResponses({ + @ApiResponse(code = 200, message = "닉네임이 사용 가능한 경우 true 반환", response = Boolean.class), + @ApiResponse(code = 409, message = "닉네임이 중복 된 경우 false 반환") + }) + @GetMapping("/nickname") + public ResponseEntity duplicateNicknameCheck(@RequestParam String nickname) { + try { + memberService.isDuplicateNickname(nickname); + } catch (DuplicateNicknameException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(true); + } + return ResponseEntity.status(HttpStatus.OK).body(false); + } + + @ApiOperation( + value = "세션을 통해 닉네임 조회", + notes = "로그인 된 회원의 닉네임을 반환합니다" + ) + @ApiResponses({ + @ApiResponse(code = 200, message = "로그인 된 회원의 닉네임 반환", response = String.class), + @ApiResponse(code = 400, message = "세션에 사용자 정보가 저장되어 있지 않을 때") + }) + @GetMapping("/auth") + public ResponseEntity findNicknameIfPresent() { + return ResponseEntity.status(HttpStatus.OK).body(memberService.findNicknameIfPresent()); + } + +} diff --git a/src/main/java/com/recordit/server/controller/RecordCategoryController.java b/src/main/java/com/recordit/server/controller/RecordCategoryController.java new file mode 100644 index 00000000..d6b9e98f --- /dev/null +++ b/src/main/java/com/recordit/server/controller/RecordCategoryController.java @@ -0,0 +1,40 @@ +package com.recordit.server.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.recordit.server.dto.record.category.RecordCategoryResponseDto; +import com.recordit.server.service.RecordCategoryService; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/record/category") +public class RecordCategoryController { + + private final RecordCategoryService recordCategoryService; + + @ApiOperation( + value = "레코드 카테고리 전체 조회", + notes = "레코드 카테고리 전체를 조회합니다." + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "API 정상 작동 / 레코드 카테고리 목록 반환", + response = RecordCategoryResponseDto.class, responseContainer = "List" + ) + }) + @GetMapping + public ResponseEntity> getAllRecordCategories() { + return ResponseEntity.ok(recordCategoryService.getAllRecordCategories()); + } + +} diff --git a/src/main/java/com/recordit/server/controller/RecordController.java b/src/main/java/com/recordit/server/controller/RecordController.java new file mode 100644 index 00000000..af89e1af --- /dev/null +++ b/src/main/java/com/recordit/server/controller/RecordController.java @@ -0,0 +1,79 @@ +package com.recordit.server.controller; + +import java.util.List; + +import javax.validation.Valid; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.dto.record.RecordDetailResponseDto; +import com.recordit.server.dto.record.WriteRecordRequestDto; +import com.recordit.server.dto.record.WriteRecordResponseDto; +import com.recordit.server.exception.ErrorMessage; +import com.recordit.server.service.RecordService; + +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/record") +public class RecordController { + + private final RecordService recordService; + + @ApiOperation( + value = "레코드 작성", + notes = "레코드를 작성합니다." + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "레코드 작성 성공" + ), + @ApiResponse( + code = 400, message = "잘못된 요청", + response = ErrorMessage.class + ) + }) + @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) + public ResponseEntity writeRecord( + @ApiParam(required = true) @RequestPart(required = true) @Valid WriteRecordRequestDto writeRecordRequestDto, + @ApiParam @RequestPart(required = false) List attachments + ) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(recordService.writeRecord(writeRecordRequestDto, attachments)); + } + + @ApiOperation( + value = "레코드 단건 조회", + notes = "레코드를 단건 조회합니다." + ) + @ApiResponses({ + @ApiResponse( + code = 200, message = "레코드 조회 성공", + response = RecordDetailResponseDto.class + ), + @ApiResponse( + code = 400, message = "레코드가 없는 경우", + response = ErrorMessage.class + ) + }) + @GetMapping("/{recordId}") + public ResponseEntity getDetailRecord( + @PathVariable("recordId") Long recordId) { + return ResponseEntity.ok().body(recordService.getDetailRecord(recordId)); + } + +} diff --git a/src/main/java/com/recordit/server/converter/LoginTypeConverter.java b/src/main/java/com/recordit/server/converter/LoginTypeConverter.java new file mode 100644 index 00000000..58d8efb3 --- /dev/null +++ b/src/main/java/com/recordit/server/converter/LoginTypeConverter.java @@ -0,0 +1,13 @@ +package com.recordit.server.converter; + +import org.springframework.core.convert.converter.Converter; + +import com.recordit.server.constant.LoginType; + +public class LoginTypeConverter implements Converter { + + @Override + public LoginType convert(String source) { + return LoginType.findByString(source); + } +} diff --git a/src/main/java/com/recordit/server/domain/BaseEntity.java b/src/main/java/com/recordit/server/domain/BaseEntity.java new file mode 100644 index 00000000..ef52bef2 --- /dev/null +++ b/src/main/java/com/recordit/server/domain/BaseEntity.java @@ -0,0 +1,33 @@ +package com.recordit.server.domain; + +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotNull; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.Getter; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseEntity { + + @NotNull + @CreatedDate + @Column(name = "CREATED_AT") + private LocalDateTime createdAt; + + @NotNull + @LastModifiedDate + @Column(name = "MODIFIED_AT") + private LocalDateTime modifiedAt; + + @Column(name = "DELETED_AT") + private LocalDateTime deletedAt; +} diff --git a/src/main/java/com/recordit/server/domain/Comment.java b/src/main/java/com/recordit/server/domain/Comment.java new file mode 100644 index 00000000..1e53f66e --- /dev/null +++ b/src/main/java/com/recordit/server/domain/Comment.java @@ -0,0 +1,56 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "COMMENT") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE COMMENT SET COMMENT.DELETED_AT = CURRENT_TIMESTAMP WHERE COMMENT.COMMENT_ID = ?") +@Getter +public class Comment extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "COMMENT_ID") + private Long id; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member writer; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RECORD_ID") + private Record record; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PARENT_COMMENT_ID") + private Comment parentComment; + + @Column(name = "CONTENT") + private String content; + + private Comment(Member writer, Record record, Comment parentComment, String content) { + this.writer = writer; + this.record = record; + this.parentComment = parentComment; + this.content = content; + } + + public static Comment of(Member writer, Record record, Comment parentComment, String content) { + return new Comment(writer, record, parentComment, content); + } +} diff --git a/src/main/java/com/recordit/server/domain/CommentReport.java b/src/main/java/com/recordit/server/domain/CommentReport.java new file mode 100644 index 00000000..2eafb6fe --- /dev/null +++ b/src/main/java/com/recordit/server/domain/CommentReport.java @@ -0,0 +1,39 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "COMMENT_REPORT") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE COMMENT_REPORT SET COMMENT_REPORT.DELETED_AT = CURRENT_TIMESTAMP WHERE COMMENT_REPORT.COMMENT_REPORT_ID = ?") +@Getter +public class CommentReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "COMMENT_REPORT_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "MEMBER_ID") + private Member reporter; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "COMMENT_ID") + private Comment reportedComment; + +} diff --git a/src/main/java/com/recordit/server/domain/ImageFile.java b/src/main/java/com/recordit/server/domain/ImageFile.java new file mode 100644 index 00000000..e5c54ad6 --- /dev/null +++ b/src/main/java/com/recordit/server/domain/ImageFile.java @@ -0,0 +1,93 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Table; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.constant.RefType; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "IMAGE_FILE") +@Table(indexes = @Index(columnList = "REF_TYPE, REF_ID")) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE IMAGE_FILE SET IMAGE_FILE.DELETED_AT = CURRENT_TIMESTAMP WHERE IMAGE_FILE.IMAGE_FILE_ID = ?") +@Getter +public class ImageFile extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "IMAGE_FILE_ID") + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "REF_TYPE") + private RefType refType; + + @Column(name = "REF_ID") + private Long refId; + + @Column(name = "DOWNLOAD_URL") + private String downloadUrl; + + @Column(name = "ORIGINAL_NAME") + private String originalName; + + @Column(name = "SAVE_NAME") + private String saveName; + + @Column(name = "EXTENSION") + private String extension; + + @Column(name = "SIZE") + private Long size; + + private ImageFile( + RefType refType, + Long refId, + String downloadUrl, + String originalName, + String saveName, + String extension, + Long size + ) { + this.refType = refType; + this.refId = refId; + this.downloadUrl = downloadUrl; + this.originalName = originalName; + this.saveName = saveName; + this.extension = extension; + this.size = size; + } + + public static ImageFile of( + RefType refType, + Long refId, + String saveUrl, + String saveName, + MultipartFile multipartFile + ) { + return new ImageFile( + refType, + refId, + saveUrl, + multipartFile.getOriginalFilename(), + saveName, + multipartFile.getOriginalFilename().substring(multipartFile.getOriginalFilename().lastIndexOf(".") + 1), + multipartFile.getSize() + ); + } +} diff --git a/src/main/java/com/recordit/server/domain/Member.java b/src/main/java/com/recordit/server/domain/Member.java new file mode 100644 index 00000000..cd85a87a --- /dev/null +++ b/src/main/java/com/recordit/server/domain/Member.java @@ -0,0 +1,59 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import com.recordit.server.constant.LoginType; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "MEMBER") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE MEMBER SET MEMBER.DELETED_AT = CURRENT_TIMESTAMP WHERE MEMBER.MEMBER_ID = ?") +@Getter +public class Member extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "MEMBER_ID") + private Long id; + + @Column(name = "USERNAME") + private String username; + + @Column(name = "PASSWORD") + private String password; + + @Column(name = "NICKNAME") + private String nickname; + + @Column(name = "OAUTH_ID") + private String oauthId; + + @Column(name = "LOGIN_TYPE") + @Enumerated(value = EnumType.STRING) + private LoginType loginType; + + private Member(String username, String password, String nickname, String oauthId, LoginType loginType) { + this.username = username; + this.password = password; + this.nickname = nickname; + this.oauthId = oauthId; + this.loginType = loginType; + } + + public static Member of(String username, String password, String nickname, String oauthId, LoginType loginType) { + return new Member(username, password, nickname, oauthId, loginType); + } +} diff --git a/src/main/java/com/recordit/server/domain/Record.java b/src/main/java/com/recordit/server/domain/Record.java new file mode 100644 index 00000000..19abccdf --- /dev/null +++ b/src/main/java/com/recordit/server/domain/Record.java @@ -0,0 +1,88 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToOne; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import com.recordit.server.dto.record.WriteRecordRequestDto; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "RECORD") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE RECORD SET RECORD.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD.RECORD_ID = ?") +@Getter +public class Record extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "RECORD_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RECORD_CATEGORY_ID") + private RecordCategory recordCategory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "MEMBER_ID") + private Member writer; + + @Column(name = "TITLE") + private String title; + + @Column(name = "CONTENT") + private String content; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RECORD_COLOR_ID") + private RecordColor recordColor; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RECORD_ICON_ID") + private RecordIcon recordIcon; + + private Record( + RecordCategory recordCategory, + Member writer, + String title, + String content, + RecordColor recordColor, + RecordIcon recordIcon + ) { + this.recordCategory = recordCategory; + this.writer = writer; + this.title = title; + this.content = content; + this.recordColor = recordColor; + this.recordIcon = recordIcon; + } + + public static Record of( + WriteRecordRequestDto writeRecordRequestDto, + RecordCategory recordCategory, + Member member, + RecordColor recordColor, + RecordIcon recordIcon + ) { + return new Record( + recordCategory, + member, + writeRecordRequestDto.getTitle(), + writeRecordRequestDto.getContent(), + recordColor, + recordIcon + ); + } +} diff --git a/src/main/java/com/recordit/server/domain/RecordCategory.java b/src/main/java/com/recordit/server/domain/RecordCategory.java new file mode 100644 index 00000000..52c30317 --- /dev/null +++ b/src/main/java/com/recordit/server/domain/RecordCategory.java @@ -0,0 +1,53 @@ +package com.recordit.server.domain; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "RECORD_CATEGORY") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE RECORD_CATEGORY SET RECORD_CATEGORY.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD_CATEGORY.RECORD_CATEGORY_ID = ?") +@Getter +public class RecordCategory extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "RECORD_CATEGORY_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PARENT_RECORD_CATEGORY_ID") + private RecordCategory parentRecordCategory; + + @Column(name = "NAME") + private String name; + + @OneToMany(mappedBy = "parentRecordCategory") + private List subcategories = new ArrayList<>(); + + private RecordCategory(RecordCategory parentRecordCategory, String name) { + this.parentRecordCategory = parentRecordCategory; + this.name = name; + } + + public static RecordCategory of(RecordCategory parentRecordCategory, String name) { + return new RecordCategory(parentRecordCategory, name); + } +} diff --git a/src/main/java/com/recordit/server/domain/RecordColor.java b/src/main/java/com/recordit/server/domain/RecordColor.java new file mode 100644 index 00000000..e894bc5a --- /dev/null +++ b/src/main/java/com/recordit/server/domain/RecordColor.java @@ -0,0 +1,33 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "RECORD_COLOR") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE RECORD_COLOR SET RECORD_COLOR.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD_COLOR.RECORD_COLOR_ID = ?") +@Getter +public class RecordColor extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "RECORD_COLOR_ID") + private Long id; + + @Column(name = "NAME") + private String name; + + @Column(name = "HEX_CODE") + private String hexCode; +} diff --git a/src/main/java/com/recordit/server/domain/RecordIcon.java b/src/main/java/com/recordit/server/domain/RecordIcon.java new file mode 100644 index 00000000..2dbb2668 --- /dev/null +++ b/src/main/java/com/recordit/server/domain/RecordIcon.java @@ -0,0 +1,30 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "RECORD_ICON") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE RECORD_ICON SET RECORD_ICON.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD_ICON.RECORD_ICON_ID = ?") +@Getter +public class RecordIcon extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "RECORD_ICON_ID") + private Long id; + + @Column(name = "NAME") + private String name; +} diff --git a/src/main/java/com/recordit/server/domain/RecordReport.java b/src/main/java/com/recordit/server/domain/RecordReport.java new file mode 100644 index 00000000..45d11552 --- /dev/null +++ b/src/main/java/com/recordit/server/domain/RecordReport.java @@ -0,0 +1,39 @@ +package com.recordit.server.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "RECORD_REPORT") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Where(clause = "DELETED_AT is null") +@SQLDelete(sql = "UPDATE RECORD_REPORT SET RECORD_REPORT.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD_REPORT.RECORD_REPORT_ID = ?") +@Getter +public class RecordReport extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "RECORD_REPORT_ID") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "MEMBER_ID") + private Member reporter; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "RECORD_ID") + private Record reportedRecord; + +} diff --git a/src/main/java/com/recordit/server/dto/comment/CommentDto.java b/src/main/java/com/recordit/server/dto/comment/CommentDto.java new file mode 100644 index 00000000..138cf6b8 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/comment/CommentDto.java @@ -0,0 +1,52 @@ +package com.recordit.server.dto.comment; + +import java.time.LocalDateTime; + +import com.recordit.server.domain.Comment; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommentDto { + + @ApiModelProperty(notes = "댓글 ID", required = true) + private Long commentId; + + @ApiModelProperty(notes = "댓글 작성자", required = true) + private String writer; + + @ApiModelProperty(notes = "댓글 내용", required = true) + private String content; + + @ApiModelProperty(notes = "첨부 이미지 URL", required = true) + private String imageUrl; + + @ApiModelProperty(notes = "하위 댓글 개수", required = false) + private Long numOfSubComment; + + @ApiModelProperty(notes = "생성 일자", required = true) + private LocalDateTime createdAt; + + @ApiModelProperty(notes = "수정 일자", required = false) + private LocalDateTime modifiedAt; + + @Builder + public CommentDto(Comment comment, String imageUrl, Long numOfSubComment) { + this.commentId = comment.getId(); + this.writer = (comment.getWriter() != null) ? comment.getWriter().getNickname() : null; + this.content = comment.getContent(); + this.imageUrl = imageUrl; + this.numOfSubComment = numOfSubComment; + this.createdAt = comment.getCreatedAt(); + this.modifiedAt = comment.getModifiedAt(); + } +} diff --git a/src/main/java/com/recordit/server/dto/comment/CommentRequestDto.java b/src/main/java/com/recordit/server/dto/comment/CommentRequestDto.java new file mode 100644 index 00000000..6719429c --- /dev/null +++ b/src/main/java/com/recordit/server/dto/comment/CommentRequestDto.java @@ -0,0 +1,30 @@ +package com.recordit.server.dto.comment; + +import javax.validation.constraints.NotNull; + +import io.swagger.annotations.ApiParam; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class CommentRequestDto { + + @ApiParam(value = "레코드 ID", required = true, example = "0") + @NotNull + private Long recordId; + + @ApiParam(value = "자식 댓글을 조회하는 경우 부모 댓글의 ID", example = "0") + private Long parentId; + + @ApiParam(value = "댓글 리스트의 요청 페이지 !주의: 0부터 시작", required = true, example = "0") + @NotNull + private Integer page; + + @ApiParam(value = "댓글 리스트의 사이즈", required = true, example = "10") + @NotNull + private Integer size; +} diff --git a/src/main/java/com/recordit/server/dto/comment/CommentResponseDto.java b/src/main/java/com/recordit/server/dto/comment/CommentResponseDto.java new file mode 100644 index 00000000..f187d5c1 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/comment/CommentResponseDto.java @@ -0,0 +1,48 @@ +package com.recordit.server.dto.comment; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.domain.Page; + +import com.recordit.server.domain.Comment; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CommentResponseDto { + + @ApiModelProperty(notes = "요청 댓글의 전체 페이지 개수", required = true) + private Integer totalPage; + + @ApiModelProperty(notes = "요청 댓글의 전체 개수", required = true) + private Long totalCount; + + @ApiModelProperty(notes = "댓글 리스트", required = true) + private List commentList; + + @Builder + public CommentResponseDto(Page comments, List imageFileUrls, List numOfSubComments) { + this.totalPage = comments.getTotalPages(); + this.totalCount = comments.getTotalElements(); + this.commentList = new ArrayList<>(); + for (int i = 0; i < comments.getContent().size(); i++) { + commentList.add( + CommentDto.builder() + .comment(comments.getContent().get(i)) + .imageUrl(imageFileUrls.get(i)) + .numOfSubComment(numOfSubComments.get(i)) + .build() + ); + } + } +} diff --git a/src/main/java/com/recordit/server/dto/comment/WriteCommentRequestDto.java b/src/main/java/com/recordit/server/dto/comment/WriteCommentRequestDto.java new file mode 100644 index 00000000..401968e5 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/comment/WriteCommentRequestDto.java @@ -0,0 +1,37 @@ +package com.recordit.server.dto.comment; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WriteCommentRequestDto { + + @ApiModelProperty(notes = "레코드의 id", required = true) + @NotNull(message = "레코드 ID를 지정해야 합니다") + private Long recordId; + + @ApiModelProperty(notes = "자식 댓글일 경우 부모 댓글의 id") + private Long parentId; + + @ApiModelProperty(notes = "댓글 내용", required = true) + @Size(max = 100, message = "댓글 내용은 100자를 넘길 수 없습니다") + private String comment; + + @Builder + public WriteCommentRequestDto(Long recordId, Long parentId, String comment) { + this.recordId = recordId; + this.parentId = parentId; + this.comment = comment; + } +} diff --git a/src/main/java/com/recordit/server/dto/comment/WriteCommentResponseDto.java b/src/main/java/com/recordit/server/dto/comment/WriteCommentResponseDto.java new file mode 100644 index 00000000..30ebc67f --- /dev/null +++ b/src/main/java/com/recordit/server/dto/comment/WriteCommentResponseDto.java @@ -0,0 +1,22 @@ +package com.recordit.server.dto.comment; + +import io.swagger.annotations.ApiModel; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WriteCommentResponseDto { + + private Long commentId; + + @Builder + public WriteCommentResponseDto(Long commentId) { + this.commentId = commentId; + } +} diff --git a/src/main/java/com/recordit/server/dto/member/GoogleAccessTokenResponseDto.java b/src/main/java/com/recordit/server/dto/member/GoogleAccessTokenResponseDto.java new file mode 100644 index 00000000..84744255 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/GoogleAccessTokenResponseDto.java @@ -0,0 +1,21 @@ +package com.recordit.server.dto.member; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class GoogleAccessTokenResponseDto { + private String accessToken; + private String expiresIn; + private String scope; + private String tokenType; + private String idToken; +} diff --git a/src/main/java/com/recordit/server/dto/member/GoogleUserInfoResponseDto.java b/src/main/java/com/recordit/server/dto/member/GoogleUserInfoResponseDto.java new file mode 100644 index 00000000..1d26b2ce --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/GoogleUserInfoResponseDto.java @@ -0,0 +1,31 @@ +package com.recordit.server.dto.member; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class GoogleUserInfoResponseDto { + private String iss; + private String azp; + private String aud; + private String sub; + private String atHash; + private String name; + private String picture; + private String givenName; + private String familyName; + private String locale; + private String iat; + private String exp; + private String alg; + private String kid; + private String typ; +} diff --git a/src/main/java/com/recordit/server/dto/member/KakaoAccessTokenResponseDto.java b/src/main/java/com/recordit/server/dto/member/KakaoAccessTokenResponseDto.java new file mode 100644 index 00000000..cb19f788 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/KakaoAccessTokenResponseDto.java @@ -0,0 +1,23 @@ +package com.recordit.server.dto.member; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class KakaoAccessTokenResponseDto { + private String accessToken; + private String tokenType; + private String refreshToken; + private String refreshTokenExpiresIn; + private String expiresIn; + private String scope; +} + diff --git a/src/main/java/com/recordit/server/dto/member/KakaoUserInfoResponseDto.java b/src/main/java/com/recordit/server/dto/member/KakaoUserInfoResponseDto.java new file mode 100644 index 00000000..e1281930 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/KakaoUserInfoResponseDto.java @@ -0,0 +1,20 @@ +package com.recordit.server.dto.member; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class KakaoUserInfoResponseDto { + private String id; + private String connectedAt; +} diff --git a/src/main/java/com/recordit/server/dto/member/LoginRequestDto.java b/src/main/java/com/recordit/server/dto/member/LoginRequestDto.java new file mode 100644 index 00000000..05b98781 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/LoginRequestDto.java @@ -0,0 +1,24 @@ +package com.recordit.server.dto.member; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LoginRequestDto { + + @ApiModelProperty(notes = "Oauth API에서 응답 받은 토큰", required = true) + private String oauthToken; + + @Builder + public LoginRequestDto(String oauthToken) { + this.oauthToken = oauthToken; + } +} diff --git a/src/main/java/com/recordit/server/dto/member/RegisterRequestDto.java b/src/main/java/com/recordit/server/dto/member/RegisterRequestDto.java new file mode 100644 index 00000000..69ce4208 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/RegisterRequestDto.java @@ -0,0 +1,31 @@ +package com.recordit.server.dto.member; + +import javax.validation.constraints.Pattern; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RegisterRequestDto { + + @ApiModelProperty(notes = "회원가입시 필요한 임시 Session", required = true) + private String registerSession; + + @ApiModelProperty(notes = "사용자 닉네임", required = true) + @Pattern(regexp = "[가-힣A-z0-9]{2,8}") + private String nickname; + + @Builder + public RegisterRequestDto(String registerSession, String nickname) { + this.registerSession = registerSession; + this.nickname = nickname; + } +} diff --git a/src/main/java/com/recordit/server/dto/member/RegisterSessionResponseDto.java b/src/main/java/com/recordit/server/dto/member/RegisterSessionResponseDto.java new file mode 100644 index 00000000..074e8f1b --- /dev/null +++ b/src/main/java/com/recordit/server/dto/member/RegisterSessionResponseDto.java @@ -0,0 +1,24 @@ +package com.recordit.server.dto.member; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RegisterSessionResponseDto { + + @ApiModelProperty(notes = "회원가입시 필요한 임시 Session", required = true) + private String registerSession; + + @Builder + public RegisterSessionResponseDto(String registerSession) { + this.registerSession = registerSession; + } +} diff --git a/src/main/java/com/recordit/server/dto/record/RecordDetailResponseDto.java b/src/main/java/com/recordit/server/dto/record/RecordDetailResponseDto.java new file mode 100644 index 00000000..25564033 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/record/RecordDetailResponseDto.java @@ -0,0 +1,53 @@ +package com.recordit.server.dto.record; + +import java.time.LocalDateTime; +import java.util.List; + +import io.swagger.annotations.ApiModel; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RecordDetailResponseDto { + private Long recordId; + private Long categoryId; + private String categoryName; + private String title; + private String content; + private String writer; + private String colorName; + private String iconName; + private LocalDateTime createdAt; + private List imageUrls; + + @Builder + public RecordDetailResponseDto( + Long recordId, + Long categoryId, + String categoryName, + String title, + String content, + String writer, + String colorName, + String iconName, + LocalDateTime createdAt, + List imageUrls + ) { + this.recordId = recordId; + this.categoryId = categoryId; + this.categoryName = categoryName; + this.title = title; + this.content = content; + this.writer = writer; + this.colorName = colorName; + this.iconName = iconName; + this.createdAt = createdAt; + this.imageUrls = imageUrls; + } +} diff --git a/src/main/java/com/recordit/server/dto/record/WriteRecordRequestDto.java b/src/main/java/com/recordit/server/dto/record/WriteRecordRequestDto.java new file mode 100644 index 00000000..5cd5b9e1 --- /dev/null +++ b/src/main/java/com/recordit/server/dto/record/WriteRecordRequestDto.java @@ -0,0 +1,50 @@ +package com.recordit.server.dto.record; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import io.swagger.annotations.ApiModel; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WriteRecordRequestDto { + @NotNull + private Long recordCategoryId; + + @Size(max = 12, message = "레코드 제목은 최대 10자 입니다.") + @NotBlank(message = "레코드 제목은 빈 값일 수 없습니다.") + private String title; + + @Size(max = 200, message = "레코드 내용은 최대 200자 입니다.") + @NotBlank(message = "레코드 내용은 빈 값일 수 없습니다.") + private String content; + + @NotBlank(message = "컬러 이름은 빈 값일 수 없습니다.") + private String colorName; + + @NotBlank(message = "아이콘 이름은 빈 값일 수 없습니다.") + private String iconName; + + @Builder + public WriteRecordRequestDto( + Long recordCategoryId, + String title, + String content, + String colorName, + String iconName + ) { + this.recordCategoryId = recordCategoryId; + this.title = title; + this.content = content; + this.colorName = colorName; + this.iconName = iconName; + } +} diff --git a/src/main/java/com/recordit/server/dto/record/WriteRecordResponseDto.java b/src/main/java/com/recordit/server/dto/record/WriteRecordResponseDto.java new file mode 100644 index 00000000..c285d07d --- /dev/null +++ b/src/main/java/com/recordit/server/dto/record/WriteRecordResponseDto.java @@ -0,0 +1,21 @@ +package com.recordit.server.dto.record; + +import io.swagger.annotations.ApiModel; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class WriteRecordResponseDto { + private Long recordId; + + @Builder + public WriteRecordResponseDto(Long recordId) { + this.recordId = recordId; + } +} diff --git a/src/main/java/com/recordit/server/dto/record/category/RecordCategoryResponseDto.java b/src/main/java/com/recordit/server/dto/record/category/RecordCategoryResponseDto.java new file mode 100644 index 00000000..9471821f --- /dev/null +++ b/src/main/java/com/recordit/server/dto/record/category/RecordCategoryResponseDto.java @@ -0,0 +1,37 @@ +package com.recordit.server.dto.record.category; + +import java.util.ArrayList; +import java.util.List; + +import com.recordit.server.domain.RecordCategory; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@ApiModel +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RecordCategoryResponseDto { + + @ApiModelProperty(notes = "레코드 카테고리 ID", required = true) + private Long id; + + @ApiModelProperty(notes = "레코드 카테고리 이름", required = true) + private String name; + + @ApiModelProperty(notes = "하위 레코드 목록", required = true) + private List subcategories = new ArrayList<>(); + + @Builder + public RecordCategoryResponseDto(RecordCategory recordCategory, List children) { + this.id = recordCategory.getId(); + this.name = recordCategory.getName(); + this.subcategories = children; + } +} diff --git a/src/main/java/com/recordit/server/environment/GoogleOauthProperties.java b/src/main/java/com/recordit/server/environment/GoogleOauthProperties.java new file mode 100644 index 00000000..ba5af10d --- /dev/null +++ b/src/main/java/com/recordit/server/environment/GoogleOauthProperties.java @@ -0,0 +1,30 @@ +package com.recordit.server.environment; + +import javax.validation.constraints.NotBlank; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "oauth.google") +@RequiredArgsConstructor +public class GoogleOauthProperties { + + @NotBlank + private final String clientId; + @NotBlank + private final String clientSecret; + @NotBlank + private final String redirectUrl; + @NotBlank + private final String tokenRequestUrl; + @NotBlank + private final String userInfoRequestUrl; + +} diff --git a/src/main/java/com/recordit/server/environment/KakaoOauthProperties.java b/src/main/java/com/recordit/server/environment/KakaoOauthProperties.java new file mode 100644 index 00000000..c0cfbedb --- /dev/null +++ b/src/main/java/com/recordit/server/environment/KakaoOauthProperties.java @@ -0,0 +1,30 @@ +package com.recordit.server.environment; + +import javax.validation.constraints.NotBlank; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "oauth.kakao") +@RequiredArgsConstructor +public class KakaoOauthProperties { + + @NotBlank + private final String clientId; + @NotBlank + private final String clientSecret; + @NotBlank + private final String redirectUrl; + @NotBlank + private final String tokenRequestUrl; + @NotBlank + private final String userInfoRequestUrl; + +} diff --git a/src/main/java/com/recordit/server/environment/S3Properties.java b/src/main/java/com/recordit/server/environment/S3Properties.java new file mode 100644 index 00000000..2338fdda --- /dev/null +++ b/src/main/java/com/recordit/server/environment/S3Properties.java @@ -0,0 +1,42 @@ +package com.recordit.server.environment; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "s3") +@RequiredArgsConstructor +public class S3Properties { + + @NotNull + private final Credentials credentials; + + @NotBlank + private final String bucket; + + @NotBlank + private final String directory; + + @NotBlank + private final String region; + + @Getter + @Validated + @RequiredArgsConstructor + public static final class Credentials { + @NotBlank + private final String accessKey; + + @NotBlank + private final String secretKey; + } +} diff --git a/src/main/java/com/recordit/server/event/S3ImageRollbackEvent.java b/src/main/java/com/recordit/server/event/S3ImageRollbackEvent.java new file mode 100644 index 00000000..4aa35b8b --- /dev/null +++ b/src/main/java/com/recordit/server/event/S3ImageRollbackEvent.java @@ -0,0 +1,18 @@ +package com.recordit.server.event; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class S3ImageRollbackEvent { + private final String rollbackFileName; + + private S3ImageRollbackEvent(String rollbackFileName) { + this.rollbackFileName = rollbackFileName; + } + + public static S3ImageRollbackEvent from(String rollbackFileName) { + return new S3ImageRollbackEvent(rollbackFileName); + } +} diff --git a/src/main/java/com/recordit/server/exception/ErrorMessage.java b/src/main/java/com/recordit/server/exception/ErrorMessage.java new file mode 100644 index 00000000..5455d1f6 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/ErrorMessage.java @@ -0,0 +1,39 @@ +package com.recordit.server.exception; + +import java.time.LocalDateTime; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ErrorMessage { + private int code; + private String errorSimpleName; + private String msg; + private LocalDateTime timestamp; + + public ErrorMessage(Exception exception, HttpStatus httpStatus, String message) { + this.code = httpStatus.value(); + this.errorSimpleName = exception.getClass().getSimpleName(); + this.msg = message; + this.timestamp = LocalDateTime.now(); + } + + public ErrorMessage(Exception exception, HttpStatus httpStatus) { + this.code = httpStatus.value(); + this.errorSimpleName = exception.getClass().getSimpleName(); + this.msg = exception.getMessage(); + this.timestamp = LocalDateTime.now(); + } + + public static ErrorMessage of(Exception exception, HttpStatus httpStatus) { + return new ErrorMessage(exception, httpStatus); + } + + public static ErrorMessage of(Exception exception, HttpStatus httpStatus, String message) { + return new ErrorMessage(exception, httpStatus, message); + } +} \ No newline at end of file diff --git a/src/main/java/com/recordit/server/exception/GlobalExceptionHandler.java b/src/main/java/com/recordit/server/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..6255998a --- /dev/null +++ b/src/main/java/com/recordit/server/exception/GlobalExceptionHandler.java @@ -0,0 +1,22 @@ +package com.recordit.server.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception) { + String message = exception.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST, message)); + } +} diff --git a/src/main/java/com/recordit/server/exception/comment/CommentExceptionHandler.java b/src/main/java/com/recordit/server/exception/comment/CommentExceptionHandler.java new file mode 100644 index 00000000..7325073d --- /dev/null +++ b/src/main/java/com/recordit/server/exception/comment/CommentExceptionHandler.java @@ -0,0 +1,48 @@ +package com.recordit.server.exception.comment; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.recordit.server.controller.CommentController; +import com.recordit.server.exception.ErrorMessage; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.record.RecordNotFoundException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice(basePackageClasses = CommentController.class) +public class CommentExceptionHandler { + + @ExceptionHandler(EmptyContentException.class) + public ResponseEntity handleEmptyContentException(EmptyContentException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(RecordNotFoundException.class) + public ResponseEntity handleRecordNotFoundException(RecordNotFoundException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(CommentNotFoundException.class) + public ResponseEntity handleCommentNotFoundException(CommentNotFoundException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleMemberNotFoundException(MemberNotFoundException exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ErrorMessage.of(exception, HttpStatus.INTERNAL_SERVER_ERROR)); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity handleIllegalStateException(IllegalStateException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } +} diff --git a/src/main/java/com/recordit/server/exception/comment/CommentNotFoundException.java b/src/main/java/com/recordit/server/exception/comment/CommentNotFoundException.java new file mode 100644 index 00000000..0c44b232 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/comment/CommentNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.comment; + +public class CommentNotFoundException extends RuntimeException { + public CommentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/comment/EmptyContentException.java b/src/main/java/com/recordit/server/exception/comment/EmptyContentException.java new file mode 100644 index 00000000..33fde658 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/comment/EmptyContentException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.comment; + +public class EmptyContentException extends RuntimeException { + public EmptyContentException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/file/EmptyFileException.java b/src/main/java/com/recordit/server/exception/file/EmptyFileException.java new file mode 100644 index 00000000..18642358 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/file/EmptyFileException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.file; + +public class EmptyFileException extends RuntimeException { + public EmptyFileException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/file/FileContentTypeNotAllowedException.java b/src/main/java/com/recordit/server/exception/file/FileContentTypeNotAllowedException.java new file mode 100644 index 00000000..d1e00c1f --- /dev/null +++ b/src/main/java/com/recordit/server/exception/file/FileContentTypeNotAllowedException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.file; + +public class FileContentTypeNotAllowedException extends RuntimeException { + public FileContentTypeNotAllowedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/file/FileExceptionHandler.java b/src/main/java/com/recordit/server/exception/file/FileExceptionHandler.java new file mode 100644 index 00000000..6e54c7bd --- /dev/null +++ b/src/main/java/com/recordit/server/exception/file/FileExceptionHandler.java @@ -0,0 +1,42 @@ +package com.recordit.server.exception.file; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.recordit.server.exception.ErrorMessage; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class FileExceptionHandler { + + @ExceptionHandler(FileInputStreamException.class) + public ResponseEntity handleFileInputStreamException(FileInputStreamException exception) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ErrorMessage.of(exception, HttpStatus.UNPROCESSABLE_ENTITY)); + } + + @ExceptionHandler(EmptyFileException.class) + public ResponseEntity handleEmptyFileException(EmptyFileException exception) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ErrorMessage.of(exception, HttpStatus.UNPROCESSABLE_ENTITY)); + } + + @ExceptionHandler(FileContentTypeNotAllowedException.class) + public ResponseEntity handleFileContentTypeNotAllowedException( + FileContentTypeNotAllowedException exception) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ErrorMessage.of(exception, HttpStatus.UNPROCESSABLE_ENTITY)); + } + + @ExceptionHandler(FileExtensionNotAllowedException.class) + public ResponseEntity handleFileExtensionNotAllowedException( + FileExtensionNotAllowedException exception + ) { + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) + .body(ErrorMessage.of(exception, HttpStatus.UNPROCESSABLE_ENTITY)); + } +} diff --git a/src/main/java/com/recordit/server/exception/file/FileExtensionNotAllowedException.java b/src/main/java/com/recordit/server/exception/file/FileExtensionNotAllowedException.java new file mode 100644 index 00000000..f2b12fb7 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/file/FileExtensionNotAllowedException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.file; + +public class FileExtensionNotAllowedException extends RuntimeException { + public FileExtensionNotAllowedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/file/FileInputStreamException.java b/src/main/java/com/recordit/server/exception/file/FileInputStreamException.java new file mode 100644 index 00000000..140d149b --- /dev/null +++ b/src/main/java/com/recordit/server/exception/file/FileInputStreamException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.file; + +public class FileInputStreamException extends RuntimeException { + public FileInputStreamException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/member/DuplicateNicknameException.java b/src/main/java/com/recordit/server/exception/member/DuplicateNicknameException.java new file mode 100644 index 00000000..d3d24718 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/DuplicateNicknameException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class DuplicateNicknameException extends RuntimeException { + public DuplicateNicknameException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/member/MemberExceptionHandler.java b/src/main/java/com/recordit/server/exception/member/MemberExceptionHandler.java new file mode 100644 index 00000000..41fc30c5 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/MemberExceptionHandler.java @@ -0,0 +1,54 @@ +package com.recordit.server.exception.member; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.RestClientException; + +import com.recordit.server.controller.MemberController; +import com.recordit.server.exception.ErrorMessage; + +@RestControllerAdvice(basePackageClasses = MemberController.class) +public class MemberExceptionHandler { + + @ExceptionHandler(NotFoundUserInfoInSessionException.class) + public ResponseEntity handleNotFoundUserInfoInSessionException( + NotFoundUserInfoInSessionException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(NotMatchLoginTypeException.class) + public ResponseEntity handleNotMatchLoginTypeException(NotMatchLoginTypeException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(NotEnteredLoginTypeException.class) + public ResponseEntity handleNotEnteredLoginTypeException(NotEnteredLoginTypeException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(DuplicateNicknameException.class) + public ResponseEntity handleDuplicateNicknameException(DuplicateNicknameException exception) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ErrorMessage.of(exception, HttpStatus.CONFLICT)); + } + + @ExceptionHandler(NotFoundRegisterSessionException.class) + public ResponseEntity handleNotFoundRegisterSessionException( + NotFoundRegisterSessionException exception) { + return ResponseEntity.status(HttpStatus.PRECONDITION_REQUIRED) + .body(ErrorMessage.of(exception, HttpStatus.PRECONDITION_REQUIRED)); + } + + @ExceptionHandler(RestClientException.class) + public ResponseEntity handleRestClientException( + RestClientException exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ErrorMessage.of(exception, HttpStatus.INTERNAL_SERVER_ERROR)); + } +} + diff --git a/src/main/java/com/recordit/server/exception/member/MemberNotFoundException.java b/src/main/java/com/recordit/server/exception/member/MemberNotFoundException.java new file mode 100644 index 00000000..fbb312ce --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/MemberNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class MemberNotFoundException extends RuntimeException { + public MemberNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/com/recordit/server/exception/member/NotEnteredLoginTypeException.java b/src/main/java/com/recordit/server/exception/member/NotEnteredLoginTypeException.java new file mode 100644 index 00000000..20d516f2 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/NotEnteredLoginTypeException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class NotEnteredLoginTypeException extends RuntimeException { + public NotEnteredLoginTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/member/NotFoundRegisterSessionException.java b/src/main/java/com/recordit/server/exception/member/NotFoundRegisterSessionException.java new file mode 100644 index 00000000..a7ee0953 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/NotFoundRegisterSessionException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class NotFoundRegisterSessionException extends RuntimeException { + public NotFoundRegisterSessionException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/member/NotFoundUserInfoInSessionException.java b/src/main/java/com/recordit/server/exception/member/NotFoundUserInfoInSessionException.java new file mode 100644 index 00000000..2ed24817 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/NotFoundUserInfoInSessionException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class NotFoundUserInfoInSessionException extends RuntimeException { + public NotFoundUserInfoInSessionException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/member/NotMatchLoginTypeException.java b/src/main/java/com/recordit/server/exception/member/NotMatchLoginTypeException.java new file mode 100644 index 00000000..355c581f --- /dev/null +++ b/src/main/java/com/recordit/server/exception/member/NotMatchLoginTypeException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.member; + +public class NotMatchLoginTypeException extends RuntimeException { + public NotMatchLoginTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/record/RecordColorNotFoundException.java b/src/main/java/com/recordit/server/exception/record/RecordColorNotFoundException.java new file mode 100644 index 00000000..b6e04d01 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/record/RecordColorNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.record; + +public class RecordColorNotFoundException extends RuntimeException { + public RecordColorNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/record/RecordExceptionHandler.java b/src/main/java/com/recordit/server/exception/record/RecordExceptionHandler.java new file mode 100644 index 00000000..61f4cba8 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/record/RecordExceptionHandler.java @@ -0,0 +1,38 @@ +package com.recordit.server.exception.record; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.recordit.server.controller.RecordController; +import com.recordit.server.exception.ErrorMessage; +import com.recordit.server.exception.member.MemberNotFoundException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice(basePackageClasses = RecordController.class) +public class RecordExceptionHandler { + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleMemberNotFoundException( + MemberNotFoundException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(RecordColorNotFoundException.class) + public ResponseEntity handleRecordColorNotFoundException( + RecordColorNotFoundException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } + + @ExceptionHandler(RecordNotFoundException.class) + public ResponseEntity handleRecordNotFoundException( + RecordNotFoundException exception) { + return ResponseEntity.badRequest() + .body(ErrorMessage.of(exception, HttpStatus.BAD_REQUEST)); + } +} + diff --git a/src/main/java/com/recordit/server/exception/record/RecordIconNotFoundException.java b/src/main/java/com/recordit/server/exception/record/RecordIconNotFoundException.java new file mode 100644 index 00000000..e45088f3 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/record/RecordIconNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.record; + +public class RecordIconNotFoundException extends RuntimeException { + public RecordIconNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/record/RecordNotFoundException.java b/src/main/java/com/recordit/server/exception/record/RecordNotFoundException.java new file mode 100644 index 00000000..88626dfa --- /dev/null +++ b/src/main/java/com/recordit/server/exception/record/RecordNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.record; + +public class RecordNotFoundException extends RuntimeException { + public RecordNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/record/category/RecordCategoryNotFoundException.java b/src/main/java/com/recordit/server/exception/record/category/RecordCategoryNotFoundException.java new file mode 100644 index 00000000..f63ec97c --- /dev/null +++ b/src/main/java/com/recordit/server/exception/record/category/RecordCategoryNotFoundException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.record.category; + +public class RecordCategoryNotFoundException extends RuntimeException { + public RecordCategoryNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/exception/redis/RedisExceptionHandler.java b/src/main/java/com/recordit/server/exception/redis/RedisExceptionHandler.java new file mode 100644 index 00000000..5b167160 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/redis/RedisExceptionHandler.java @@ -0,0 +1,22 @@ +package com.recordit.server.exception.redis; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.recordit.server.exception.ErrorMessage; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class RedisExceptionHandler { + + @ExceptionHandler(RedisParsingException.class) + public ResponseEntity handleRedisParsingException(RedisParsingException exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ErrorMessage.of(exception, HttpStatus.INTERNAL_SERVER_ERROR)); + } + +} diff --git a/src/main/java/com/recordit/server/exception/redis/RedisParsingException.java b/src/main/java/com/recordit/server/exception/redis/RedisParsingException.java new file mode 100644 index 00000000..03d5a7d2 --- /dev/null +++ b/src/main/java/com/recordit/server/exception/redis/RedisParsingException.java @@ -0,0 +1,7 @@ +package com.recordit.server.exception.redis; + +public class RedisParsingException extends RuntimeException { + public RedisParsingException(String message) { + super(message); + } +} diff --git a/src/main/java/com/recordit/server/logger/HTTPLogFilter.java b/src/main/java/com/recordit/server/logger/HTTPLogFilter.java new file mode 100644 index 00000000..b8ab47ba --- /dev/null +++ b/src/main/java/com/recordit/server/logger/HTTPLogFilter.java @@ -0,0 +1,130 @@ +package com.recordit.server.logger; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.stereotype.Component; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class HTTPLogFilter implements Filter { + + @Override + public void doFilter( + ServletRequest request, + ServletResponse response, + FilterChain chain + ) throws IOException, ServletException { + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper( + (HttpServletRequest)request + ); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper( + (HttpServletResponse)response + ); + + long start = System.currentTimeMillis(); + chain.doFilter(requestWrapper, responseWrapper); + long end = System.currentTimeMillis(); + + if (isInExcludeURIForLog(requestWrapper)) { + responseWrapper.copyBodyToResponse(); + return; + } + + log.info("\n" + + "[REQUEST] {} - {} {} - {}ms\n" + + "Headers : {}\n" + + "RequestParameter : {}\n" + + "RequestBody : {}\n" + + "Response : {}\n", + ((HttpServletRequest)request).getMethod(), + ((HttpServletRequest)request).getRequestURI(), + responseWrapper.getStatus(), + (end - start), + getHeaders(requestWrapper), + getRequestParameter(requestWrapper), + getRequestBody(requestWrapper), + getResponseBody(responseWrapper)); + } + + private boolean isInExcludeURIForLog(ContentCachingRequestWrapper requestWrapper) { + if ((requestWrapper.getRequestURI().contains("/api/swagger"))) { + return true; + } + if ("/v2/api-docs".equals(requestWrapper.getRequestURI())) { + return true; + } + return false; + } + + private Map getHeaders(ContentCachingRequestWrapper requestWrapper) { + Map headerMap = new HashMap<>(); + + Enumeration headerArray = requestWrapper.getHeaderNames(); + while (headerArray.hasMoreElements()) { + String headerName = (String)headerArray.nextElement(); + headerMap.put(headerName, requestWrapper.getHeader(headerName)); + } + return headerMap; + } + + private String getRequestParameter(ContentCachingRequestWrapper requestWrapper) { + Map parameterMap = requestWrapper.getParameterMap(); + if (parameterMap.size() == 0) { + return "-"; + } + + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : parameterMap.entrySet()) { + sb.append(entry.getKey() + "="); + for (String value : entry.getValue()) { + sb.append(value + ", "); + } + sb.replace(sb.length() - 2, sb.length(), " | "); + } + sb.replace(sb.length() - 2, sb.length(), ""); + return sb.toString(); + } + + private String getRequestBody(ContentCachingRequestWrapper requestWrapper) { + if (requestWrapper != null) { + byte[] buf = requestWrapper.getContentAsByteArray(); + if (buf.length > 0) { + try { + return new String(buf, 0, buf.length, requestWrapper.getCharacterEncoding()); + } catch (UnsupportedEncodingException e) { + return " - "; + } + } + } + return " - "; + } + + private String getResponseBody(ContentCachingResponseWrapper responseWrapper) throws IOException { + String payload = null; + if (responseWrapper != null) { + responseWrapper.setCharacterEncoding("UTF-8"); + byte[] buf = responseWrapper.getContentAsByteArray(); + if (buf.length > 0) { + payload = new String(buf, 0, buf.length, responseWrapper.getCharacterEncoding()); + responseWrapper.copyBodyToResponse(); + } + } + return null == payload ? " - " : payload; + } +} diff --git a/src/main/java/com/recordit/server/logger/MDCLoggingFilter.java b/src/main/java/com/recordit/server/logger/MDCLoggingFilter.java new file mode 100644 index 00000000..2217516e --- /dev/null +++ b/src/main/java/com/recordit/server/logger/MDCLoggingFilter.java @@ -0,0 +1,32 @@ +package com.recordit.server.logger; + +import java.io.IOException; +import java.util.UUID; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class MDCLoggingFilter implements Filter { + + @Override + public void doFilter( + final ServletRequest request, + final ServletResponse response, + final FilterChain chain + ) throws IOException, ServletException { + final UUID uuid = UUID.randomUUID(); + MDC.put("trace_id", uuid.toString().substring(0, uuid.toString().indexOf("-"))); + chain.doFilter(request, response); + MDC.clear(); + } +} diff --git a/src/main/java/com/recordit/server/repository/CommentRepository.java b/src/main/java/com/recordit/server/repository/CommentRepository.java new file mode 100644 index 00000000..d2fecbdc --- /dev/null +++ b/src/main/java/com/recordit/server/repository/CommentRepository.java @@ -0,0 +1,24 @@ +package com.recordit.server.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.recordit.server.domain.Comment; +import com.recordit.server.domain.Record; + +public interface CommentRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"writer"}) + @Query("select c from COMMENT c left join c.writer where c.record = :record and c.parentComment is null") + Page findAllByRecordWithPagination(@Param("record") Record record, Pageable pageable); + + @EntityGraph(attributePaths = "writer") + @Query("select c from COMMENT c left join c.writer where c.parentComment = :parentComment") + Page findAllByParentComment(@Param("parentComment") Comment parentComment, Pageable pageable); + + Long countAllByParentComment(Comment parentComment); +} diff --git a/src/main/java/com/recordit/server/repository/ImageFileRepository.java b/src/main/java/com/recordit/server/repository/ImageFileRepository.java new file mode 100644 index 00000000..fc27da84 --- /dev/null +++ b/src/main/java/com/recordit/server/repository/ImageFileRepository.java @@ -0,0 +1,16 @@ +package com.recordit.server.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.recordit.server.constant.RefType; +import com.recordit.server.domain.ImageFile; + +public interface ImageFileRepository extends JpaRepository { + + Optional findByRefTypeAndRefId(RefType refType, Long refId); + + List findAllByRefTypeAndRefId(RefType refType, Long refId); +} diff --git a/src/main/java/com/recordit/server/repository/MemberRepository.java b/src/main/java/com/recordit/server/repository/MemberRepository.java new file mode 100644 index 00000000..236705b8 --- /dev/null +++ b/src/main/java/com/recordit/server/repository/MemberRepository.java @@ -0,0 +1,14 @@ +package com.recordit.server.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.recordit.server.domain.Member; + +public interface MemberRepository extends JpaRepository { + + Optional findByOauthId(String oauthId); + + boolean existsByNickname(String nickname); +} diff --git a/src/main/java/com/recordit/server/repository/RecordCategoryRepository.java b/src/main/java/com/recordit/server/repository/RecordCategoryRepository.java new file mode 100644 index 00000000..ca602a8e --- /dev/null +++ b/src/main/java/com/recordit/server/repository/RecordCategoryRepository.java @@ -0,0 +1,14 @@ +package com.recordit.server.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.recordit.server.domain.RecordCategory; + +public interface RecordCategoryRepository extends JpaRepository { + + @Query("select rc from RECORD_CATEGORY rc left join fetch rc.parentRecordCategory") + List findAllFetchDepthIsOne(); +} diff --git a/src/main/java/com/recordit/server/repository/RecordColorRepository.java b/src/main/java/com/recordit/server/repository/RecordColorRepository.java new file mode 100644 index 00000000..ee605036 --- /dev/null +++ b/src/main/java/com/recordit/server/repository/RecordColorRepository.java @@ -0,0 +1,11 @@ +package com.recordit.server.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.recordit.server.domain.RecordColor; + +public interface RecordColorRepository extends JpaRepository { + Optional findByName(String colorName); +} diff --git a/src/main/java/com/recordit/server/repository/RecordIconRepository.java b/src/main/java/com/recordit/server/repository/RecordIconRepository.java new file mode 100644 index 00000000..d8b0ac41 --- /dev/null +++ b/src/main/java/com/recordit/server/repository/RecordIconRepository.java @@ -0,0 +1,11 @@ +package com.recordit.server.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.recordit.server.domain.RecordIcon; + +public interface RecordIconRepository extends JpaRepository { + Optional findByName(String iconName); +} diff --git a/src/main/java/com/recordit/server/repository/RecordRepository.java b/src/main/java/com/recordit/server/repository/RecordRepository.java new file mode 100644 index 00000000..2fafe07a --- /dev/null +++ b/src/main/java/com/recordit/server/repository/RecordRepository.java @@ -0,0 +1,12 @@ +package com.recordit.server.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.recordit.server.domain.Record; + +public interface RecordRepository extends JpaRepository { + // @Query("select r from RECORD r join fetch r.writer join fetch r.recordColor join fetch r.recordIcon" + // + " where r.id = :id") + // Optional findById(Long id); + +} diff --git a/src/main/java/com/recordit/server/service/CommentService.java b/src/main/java/com/recordit/server/service/CommentService.java new file mode 100644 index 00000000..cae4d536 --- /dev/null +++ b/src/main/java/com/recordit/server/service/CommentService.java @@ -0,0 +1,145 @@ +package com.recordit.server.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.constant.RefType; +import com.recordit.server.domain.Comment; +import com.recordit.server.domain.Member; +import com.recordit.server.domain.Record; +import com.recordit.server.dto.comment.CommentRequestDto; +import com.recordit.server.dto.comment.CommentResponseDto; +import com.recordit.server.dto.comment.WriteCommentRequestDto; +import com.recordit.server.dto.comment.WriteCommentResponseDto; +import com.recordit.server.exception.comment.CommentNotFoundException; +import com.recordit.server.exception.comment.EmptyContentException; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.member.NotFoundUserInfoInSessionException; +import com.recordit.server.exception.record.RecordNotFoundException; +import com.recordit.server.repository.CommentRepository; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.repository.RecordRepository; +import com.recordit.server.util.SessionUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CommentService { + + private final ImageFileService imageFileService; + private final CommentRepository commentRepository; + private final MemberRepository memberRepository; + private final RecordRepository recordRepository; + private final SessionUtil sessionUtil; + + @Transactional + public WriteCommentResponseDto writeComment( + WriteCommentRequestDto writeCommentRequestDto, + MultipartFile attachment + ) { + validateEmptyContent(writeCommentRequestDto, attachment); + + Member writer = findWriterIfPresent(); + + Record record = recordRepository.findById(writeCommentRequestDto.getRecordId()) + .orElseThrow(() -> new RecordNotFoundException("댓글을 작성할 레코드가 존재하지 않습니다.")); + + Comment parentComment = (writeCommentRequestDto.getParentId() == null) ? null : + commentRepository.findById(writeCommentRequestDto.getParentId()) + .orElseThrow(() -> new CommentNotFoundException("지정한 부모 댓글이 존재하지 않습니다.")); + validateParentHasParent(parentComment); + + Comment saveComment = commentRepository.save( + Comment.of( + writer, + record, + parentComment, + writeCommentRequestDto.getComment() + ) + ); + + if (!imageFileService.isEmptyFile(attachment)) { + imageFileService.saveAttachmentFile(RefType.COMMENT, saveComment.getId(), attachment); + } + + log.info("저장한 댓글 ID : {}", saveComment.getId()); + + return WriteCommentResponseDto.builder() + .commentId(saveComment.getId()) + .build(); + } + + @Transactional(readOnly = true) + public CommentResponseDto getCommentsBy(CommentRequestDto commentRequestDto) { + Page findComments; + PageRequest pageRequest = PageRequest.of( + commentRequestDto.getPage(), + commentRequestDto.getSize(), + Sort.Direction.DESC, + "createdAt" + ); + + if (commentRequestDto.getParentId() == null) { + Record findRecord = recordRepository.findById(commentRequestDto.getRecordId()) + .orElseThrow(() -> new RecordNotFoundException("레코드 정보를 찾을 수 없습니다.")); + + findComments = commentRepository.findAllByRecordWithPagination(findRecord, pageRequest); + } else { + Comment parentComment = commentRepository.findById(commentRequestDto.getParentId()) + .orElseThrow(() -> new CommentNotFoundException("지정한 부모 댓글이 존재하지 않습니다.")); + validateParentHasParent(parentComment); + + findComments = commentRepository.findAllByParentComment(parentComment, pageRequest); + } + + List numOfSubComments = findComments.stream() + .map(comment -> commentRepository.countAllByParentComment(comment)) + .collect(Collectors.toList()); + + List imageFileUrls = findComments.stream() + .map(comment -> imageFileService.getImageFile(RefType.COMMENT, comment.getId())) + .collect(Collectors.toList()); + + return CommentResponseDto.builder() + .comments(findComments) + .imageFileUrls(imageFileUrls) + .numOfSubComments(numOfSubComments) + .build(); + } + + private void validateEmptyContent( + WriteCommentRequestDto writeCommentRequestDto, + MultipartFile attachment + ) { + if (imageFileService.isEmptyFile(attachment) && !StringUtils.hasText(writeCommentRequestDto.getComment())) { + throw new EmptyContentException("댓글 내용과 이미지 파일 모두 비어있을 수 없습니다."); + } + } + + private void validateParentHasParent(Comment parentComment) { + if (parentComment != null && parentComment.getParentComment() != null) { + throw new IllegalStateException("부모 댓글은 부모를 가질 수 없습니다."); + } + } + + private Member findWriterIfPresent() { + try { + Long userIdInSession = sessionUtil.findUserIdBySession(); + return memberRepository.findById(userIdInSession) + .orElseThrow(() -> new MemberNotFoundException("세션에 저장된 사용자가 DB에 존재하지 않습니다.")); + } catch (NotFoundUserInfoInSessionException e) { + return null; + } + } +} diff --git a/src/main/java/com/recordit/server/service/ImageFileService.java b/src/main/java/com/recordit/server/service/ImageFileService.java new file mode 100644 index 00000000..b3a72bc1 --- /dev/null +++ b/src/main/java/com/recordit/server/service/ImageFileService.java @@ -0,0 +1,152 @@ +package com.recordit.server.service; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.constant.ImageFileExtension; +import com.recordit.server.constant.RefType; +import com.recordit.server.domain.ImageFile; +import com.recordit.server.event.S3ImageRollbackEvent; +import com.recordit.server.exception.file.FileContentTypeNotAllowedException; +import com.recordit.server.exception.file.FileExtensionNotAllowedException; +import com.recordit.server.repository.ImageFileRepository; +import com.recordit.server.util.S3Uploader; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ImageFileService { + + private final ImageFileRepository imageFileRepository; + private final S3Uploader s3Uploader; + private final ApplicationEventPublisher applicationEventPublisher; + + @Transactional + public String saveAttachmentFile( + @NonNull RefType refType, + @NonNull Long refId, + @NonNull MultipartFile attachment + ) { + if (attachment.isEmpty()) { + return null; + } + validateImageContentType(attachment); + validateFileExtension(attachment); + + String saveFileName = s3Uploader.upload(attachment); + log.info("S3에 저장한 파일 이름 : {}", saveFileName); + String saveFileUrl = s3Uploader.getUrlByFileName(saveFileName); + log.info("S3에 저장한 URL : {}", saveFileUrl); + applicationEventPublisher.publishEvent(S3ImageRollbackEvent.from(saveFileName)); + + imageFileRepository.save( + ImageFile.of( + refType, + refId, + saveFileUrl, + saveFileName, + attachment + ) + ); + + return saveFileUrl; + } + + @Transactional + public List saveAttachmentFiles( + @NonNull RefType refType, + @NonNull Long refId, + @NonNull List attachments + ) { + return attachments.stream() + .map(attachment -> saveAttachmentFile(refType, refId, attachment)) + .filter(saveUrl -> saveUrl != null) + .collect(Collectors.toList()); + } + + @TransactionalEventListener(classes = S3ImageRollbackEvent.class, phase = TransactionPhase.AFTER_ROLLBACK) + public void handleRollback(S3ImageRollbackEvent event) { + log.warn("S3에 업로드한 이미지 파일 롤백 : {}", event); + s3Uploader.delete(event.getRollbackFileName()); + } + + @Transactional(readOnly = true) + public String getImageFile( + @NonNull RefType refType, + @NonNull Long refId + ) { + Optional findImageFile = imageFileRepository.findByRefTypeAndRefId(refType, refId); + if (findImageFile.isPresent()) { + return findImageFile.get().getDownloadUrl(); + } + return null; + } + + @Transactional(readOnly = true) + public List getImageFiles( + @NonNull RefType refType, + @NonNull Long refId + ) { + return imageFileRepository.findAllByRefTypeAndRefId(refType, refId).stream() + .map(ImageFile::getDownloadUrl) + .collect(Collectors.toList()); + } + + @Transactional + public void deleteAttachmentFiles(List attachmentFileNames) { + for (String attachmentFileName : attachmentFileNames) { + s3Uploader.delete(attachmentFileName); + log.info("저장한 이미지 파일 삭제 : {}", attachmentFileName); + } + } + + private void validateImageContentType(MultipartFile multipartFile) { + if (!multipartFile.getContentType().startsWith("image")) { + log.warn("요청 파일 ContentType : {}", multipartFile.getContentType()); + throw new FileContentTypeNotAllowedException("이미지 파일이 아닙니다."); + } + } + + private void validateFileExtension(MultipartFile multipartFile) { + String extension = multipartFile + .getOriginalFilename() + .substring(multipartFile.getOriginalFilename().lastIndexOf(".") + 1); + Arrays.stream(ImageFileExtension.values()) + .filter(imageFileExtension -> imageFileExtension.name().equals(extension)) + .findFirst() + .orElseThrow(() -> new FileExtensionNotAllowedException("지원하지 않은 파일 확장자입니다.")); + } + + public boolean isEmptyFile(MultipartFile multipartFile) { + if (multipartFile == null || multipartFile.isEmpty()) { + return true; + } + return false; + } + + public boolean isEmptyFile(List multipartFiles) { + if (multipartFiles == null) { + return true; + } + for (MultipartFile multipartFile : multipartFiles) { + if (isEmptyFile(multipartFile)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/recordit/server/service/MemberService.java b/src/main/java/com/recordit/server/service/MemberService.java new file mode 100644 index 00000000..81ff8250 --- /dev/null +++ b/src/main/java/com/recordit/server/service/MemberService.java @@ -0,0 +1,101 @@ +package com.recordit.server.service; + +import static com.recordit.server.constant.RegisterSessionConstants.*; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.domain.Member; +import com.recordit.server.dto.member.LoginRequestDto; +import com.recordit.server.dto.member.RegisterRequestDto; +import com.recordit.server.dto.member.RegisterSessionResponseDto; +import com.recordit.server.exception.member.DuplicateNicknameException; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.member.NotFoundRegisterSessionException; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.service.oauth.OauthService; +import com.recordit.server.service.oauth.OauthServiceLocator; +import com.recordit.server.util.RedisManager; +import com.recordit.server.util.SessionUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MemberService { + private final OauthServiceLocator oauthServiceLocator; + private final MemberRepository memberRepository; + private final SessionUtil sessionUtil; + private final RedisManager redisManager; + + @Transactional + public Optional oauthLogin(LoginType loginType, LoginRequestDto loginRequestDto) { + OauthService oauthService = oauthServiceLocator.getOauthServiceByLoginType(loginType); + + String oauthId = oauthService.getUserInfoByOauthToken(loginRequestDto.getOauthToken()); + log.info("Oauth 로그인 응답 ID : {}", oauthId); + + Optional findMember = memberRepository.findByOauthId(oauthId); + + if (findMember.isPresent()) { + sessionUtil.saveUserIdInSession(findMember.get().getId()); + log.info("사용자 세션 저장 ID : {}", findMember.get().getId()); + return Optional.empty(); + } + + String registerSessionUUID = UUID.randomUUID().toString(); + redisManager.set(PREFIX_REGISTER_SESSION + registerSessionUUID, oauthId, TIMEOUT, TimeUnit.MINUTES); + log.info("사용자 회원가입 세션 Redis에 저장 : {}", registerSessionUUID); + return Optional.of(RegisterSessionResponseDto.builder() + .registerSession(registerSessionUUID) + .build()); + } + + @Transactional + public void oauthRegister(LoginType loginType, RegisterRequestDto registerRequestDto) { + Optional oauthId = redisManager.get( + PREFIX_REGISTER_SESSION + registerRequestDto.getRegisterSession(), + String.class + ); + if (oauthId.isEmpty()) { + log.warn("요청한 Register Session이 존재하지 않음 : {}", registerRequestDto.getRegisterSession()); + throw new NotFoundRegisterSessionException("Oauth 회원가입을 위한 register_session이 존재하지 않습니다."); + } + + isDuplicateNickname(registerRequestDto.getNickname()); + Member saveMember = memberRepository.save( + Member.of( + null, + null, + registerRequestDto.getNickname(), + oauthId.get(), + loginType + ) + ); + sessionUtil.saveUserIdInSession(saveMember.getId()); + redisManager.delete(PREFIX_REGISTER_SESSION + registerRequestDto.getRegisterSession()); + } + + @Transactional(readOnly = true) + public void isDuplicateNickname(String nickname) { + if (memberRepository.existsByNickname(nickname)) { + log.warn("중복된 닉네임이 존재함 : {}", nickname); + throw new DuplicateNicknameException("중복된 닉네임이 존재합니다."); + } + } + + @Transactional(readOnly = true) + public String findNicknameIfPresent() { + Long userIdBySession = sessionUtil.findUserIdBySession(); + Member member = memberRepository.findById(userIdBySession) + .orElseThrow(() -> new MemberNotFoundException("회원 정보를 찾을 수 없습니다.")); + return member.getNickname(); + } +} diff --git a/src/main/java/com/recordit/server/service/RecordCategoryService.java b/src/main/java/com/recordit/server/service/RecordCategoryService.java new file mode 100644 index 00000000..ed36bdcf --- /dev/null +++ b/src/main/java/com/recordit/server/service/RecordCategoryService.java @@ -0,0 +1,64 @@ +package com.recordit.server.service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.recordit.server.domain.RecordCategory; +import com.recordit.server.dto.record.category.RecordCategoryResponseDto; +import com.recordit.server.repository.RecordCategoryRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RecordCategoryService { + private final RecordCategoryRepository recordCategoryRepository; + + @Transactional(readOnly = true) + @Cacheable(value = "Categories") + public List getAllRecordCategories() { + List findRecordCategories = recordCategoryRepository.findAllFetchDepthIsOne(); + + LinkedHashMap> parentToChildren = new LinkedHashMap<>(); + + // 부모이면서 자식이 null이 아닌 Map 생성 + parentToChildren.putAll(findRecordCategories.stream() + .filter(recordCategory -> recordCategory.getParentRecordCategory() != null) + .collect(Collectors.groupingBy(RecordCategory::getParentRecordCategory))); + + // 부모이면서 자식이 null인 객체 Map에 추가 + findRecordCategories.stream() + .filter(recordCategory -> recordCategory.getParentRecordCategory() == null) + .forEach(recordCategory -> parentToChildren.putIfAbsent(recordCategory, Collections.emptyList())); + + List result = new ArrayList<>(); + for (RecordCategory parent : parentToChildren.keySet()) { + // 자식 객체들을 자식 DTO List로 변환 + List children = parentToChildren.get(parent) + .stream() + .map( + child -> RecordCategoryResponseDto.builder() + .recordCategory(child) + .children(Collections.emptyList()) + .build() + ) + .collect(Collectors.toList()); + + // 자식 DTO 객체들을 부모 DTO 객체에 추가 후 result에 담음 + RecordCategoryResponseDto parentDto = RecordCategoryResponseDto.builder() + .recordCategory(parent) + .children(children) + .build(); + result.add(parentDto); + } + + return result; + } +} diff --git a/src/main/java/com/recordit/server/service/RecordService.java b/src/main/java/com/recordit/server/service/RecordService.java new file mode 100644 index 00000000..57259672 --- /dev/null +++ b/src/main/java/com/recordit/server/service/RecordService.java @@ -0,0 +1,128 @@ +package com.recordit.server.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.constant.RefType; +import com.recordit.server.domain.ImageFile; +import com.recordit.server.domain.Member; +import com.recordit.server.domain.Record; +import com.recordit.server.domain.RecordCategory; +import com.recordit.server.domain.RecordColor; +import com.recordit.server.domain.RecordIcon; +import com.recordit.server.dto.record.RecordDetailResponseDto; +import com.recordit.server.dto.record.WriteRecordRequestDto; +import com.recordit.server.dto.record.WriteRecordResponseDto; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.record.RecordColorNotFoundException; +import com.recordit.server.exception.record.RecordIconNotFoundException; +import com.recordit.server.exception.record.RecordNotFoundException; +import com.recordit.server.exception.record.category.RecordCategoryNotFoundException; +import com.recordit.server.repository.ImageFileRepository; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.repository.RecordCategoryRepository; +import com.recordit.server.repository.RecordColorRepository; +import com.recordit.server.repository.RecordIconRepository; +import com.recordit.server.repository.RecordRepository; +import com.recordit.server.util.SessionUtil; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RecordService { + private final ImageFileRepository imageFileRepository; + private final SessionUtil sessionUtil; + private final MemberRepository memberRepository; + private final RecordCategoryRepository recordCategoryRepository; + private final RecordColorRepository recordColorRepository; + private final RecordIconRepository recordIconRepository; + private final RecordRepository recordRepository; + private final ImageFileService imageFileService; + + @Transactional + public WriteRecordResponseDto writeRecord(WriteRecordRequestDto writeRecordRequestDto, + List attachments) { + + Long userIdBySession = sessionUtil.findUserIdBySession(); + log.info("세션에서 찾은 사용자 ID : {}", userIdBySession); + + Member member = memberRepository.findById(userIdBySession) + .orElseThrow(() -> new MemberNotFoundException("회원 정보를 찾을 수 없습니다.")); + + RecordCategory recordCategory = recordCategoryRepository.findById(writeRecordRequestDto.getRecordCategoryId()) + .orElseThrow(() -> new RecordCategoryNotFoundException("카테고리 정보를 찾을 수 없습니다.")); + + RecordColor recordColor = recordColorRepository.findByName(writeRecordRequestDto.getColorName()) + .orElseThrow(() -> new RecordColorNotFoundException("컬러 정보를 찾을 수 없습니다.")); + + RecordIcon recordIcon = recordIconRepository.findByName(writeRecordRequestDto.getIconName()) + .orElseThrow(() -> new RecordIconNotFoundException("아이콘 정보를 찾을 수 없습니다.")); + + Record record = Record.of( + writeRecordRequestDto, + recordCategory, + member, + recordColor, + recordIcon + ); + + Long recordId = recordRepository.save(record).getId(); + log.info("저장한 레코드 ID : ", recordId); + + if (!imageFileService.isEmptyFile(attachments)) { + List urls = imageFileService.saveAttachmentFiles(RefType.RECORD, recordId, attachments); + log.info("저장된 이미지 urls : {}", urls); + } + + return WriteRecordResponseDto.builder() + .recordId(recordId) + .build(); + } + + @Transactional(readOnly = true) + public RecordDetailResponseDto getDetailRecord(Long recordId) { + Record record = recordRepository.findById(recordId) + .orElseThrow(() -> new RecordNotFoundException("레코드 정보를 찾을 수 없습니다.")); + + List imageUrls = new ArrayList<>(); + + Optional> optionalImageFileList = Optional.of( + imageFileRepository.findAllByRefTypeAndRefId( + RefType.RECORD, + recordId + ) + ); + + if (!optionalImageFileList.isEmpty()) { + + optionalImageFileList.get().stream() + .forEach( + (imageFile) -> { + imageUrls.add(imageFile.getDownloadUrl()); + } + ); + + } + + return RecordDetailResponseDto.builder() + .recordId(record.getId()) + .categoryId(record.getRecordCategory().getId()) + .categoryName(record.getRecordCategory().getName()) + .title(record.getTitle()) + .content(record.getContent()) + .writer(record.getWriter().getNickname()) + .colorName(record.getRecordColor().getName()) + .iconName(record.getRecordIcon().getName()) + .createdAt(record.getCreatedAt()) + .imageUrls(imageUrls) + .build(); + } +} diff --git a/src/main/java/com/recordit/server/service/oauth/GoogleOauthService.java b/src/main/java/com/recordit/server/service/oauth/GoogleOauthService.java new file mode 100644 index 00000000..70b7efd6 --- /dev/null +++ b/src/main/java/com/recordit/server/service/oauth/GoogleOauthService.java @@ -0,0 +1,81 @@ +package com.recordit.server.service.oauth; + +import static com.recordit.server.constant.OauthConstants.*; + +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.dto.member.GoogleAccessTokenResponseDto; +import com.recordit.server.dto.member.GoogleUserInfoResponseDto; +import com.recordit.server.environment.GoogleOauthProperties; +import com.recordit.server.util.CustomObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GoogleOauthService implements OauthService { + + private final GoogleOauthProperties googleOauthProperties; + + @Override + @Transactional(readOnly = true) + public LoginType getLoginType() { + return LoginType.GOOGLE; + } + + @Override + @Transactional(readOnly = true) + public String getUserInfoByOauthToken(String oauthToken) { + GoogleAccessTokenResponseDto googleAccessTokenResponseDto = requestAccessToken(oauthToken); + GoogleUserInfoResponseDto googleUserInfoResponseDto = requestUserInfo(googleAccessTokenResponseDto); + return googleUserInfoResponseDto.getSub(); + } + + @Transactional(readOnly = true) + protected GoogleAccessTokenResponseDto requestAccessToken(String oauthToken) { + String params = UriComponentsBuilder.fromUriString(googleOauthProperties.getTokenRequestUrl()) + .queryParam(CODE.key, oauthToken) + .queryParam(CLIENT_ID.key, googleOauthProperties.getClientId()) + .queryParam(CLIENT_SECRET.key, googleOauthProperties.getClientSecret()) + .queryParam(REDIRECT_URI.key, googleOauthProperties.getRedirectUrl()) + .queryParam(GRANT_TYPE.key, getFixGrantType()) + .toUriString(); + log.info("구글 Oauth AccessToken 요청 : {}", params); + + ResponseEntity exchange = new RestTemplate().exchange( + params, + HttpMethod.POST, + null, + String.class + ); + log.info("구글 Oauth AccessToken 응답 : {}", exchange); + + return CustomObjectMapper.readValue(exchange.getBody(), GoogleAccessTokenResponseDto.class); + } + + @Transactional(readOnly = true) + protected GoogleUserInfoResponseDto requestUserInfo(GoogleAccessTokenResponseDto googleAccessTokenResponseDto) { + String uri = UriComponentsBuilder.fromUriString(googleOauthProperties.getUserInfoRequestUrl()) + .queryParam(ID_TOKEN.key, googleAccessTokenResponseDto.getIdToken()) + .toUriString(); + log.info("구글 Oauth UserInfo 요청 : {}", uri); + + ResponseEntity exchange = new RestTemplate().exchange( + uri, + HttpMethod.GET, + null, + String.class + ); + log.info("구글 Oauth UserInfo 응답 : {}", exchange); + + return CustomObjectMapper.readValue(exchange.getBody(), GoogleUserInfoResponseDto.class); + } +} diff --git a/src/main/java/com/recordit/server/service/oauth/KakaoOauthService.java b/src/main/java/com/recordit/server/service/oauth/KakaoOauthService.java new file mode 100644 index 00000000..770bc36b --- /dev/null +++ b/src/main/java/com/recordit/server/service/oauth/KakaoOauthService.java @@ -0,0 +1,83 @@ +package com.recordit.server.service.oauth; + +import static com.recordit.server.constant.OauthConstants.*; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.dto.member.KakaoAccessTokenResponseDto; +import com.recordit.server.dto.member.KakaoUserInfoResponseDto; +import com.recordit.server.environment.KakaoOauthProperties; +import com.recordit.server.util.CustomObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class KakaoOauthService implements OauthService { + + private final KakaoOauthProperties kakaoOauthProperties; + + @Override + @Transactional(readOnly = true) + public LoginType getLoginType() { + return LoginType.KAKAO; + } + + @Override + @Transactional(readOnly = true) + public String getUserInfoByOauthToken(String oauthToken) { + KakaoAccessTokenResponseDto kakaoAccessTokenResponseDto = requestAccessToken(oauthToken); + KakaoUserInfoResponseDto kakaoUserInfoResponseDto = requestUserInfo(kakaoAccessTokenResponseDto); + return String.valueOf(kakaoUserInfoResponseDto.getId()); + } + + @Transactional(readOnly = true) + protected KakaoAccessTokenResponseDto requestAccessToken(String oauthToken) { + String params = UriComponentsBuilder.fromUriString(kakaoOauthProperties.getTokenRequestUrl()) + .queryParam(CODE.key, oauthToken) + .queryParam(CLIENT_ID.key, kakaoOauthProperties.getClientId()) + .queryParam(CLIENT_SECRET.key, kakaoOauthProperties.getClientSecret()) + .queryParam(REDIRECT_URI.key, kakaoOauthProperties.getRedirectUrl()) + .queryParam(GRANT_TYPE.key, getFixGrantType()) + .toUriString(); + log.info("카카오 Oauth AccessToken 요청 : {}", params); + + ResponseEntity exchange = new RestTemplate().exchange( + params, + HttpMethod.POST, + null, + String.class + ); + log.info("카카오 Oauth AccessToken 응답 : {}", exchange); + + return CustomObjectMapper.readValue(exchange.getBody(), KakaoAccessTokenResponseDto.class); + } + + @Transactional(readOnly = true) + protected KakaoUserInfoResponseDto requestUserInfo(KakaoAccessTokenResponseDto kakaoAccessTokenResponseDto) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(AUTHORIZATION.key, getFixPrefixJwt() + kakaoAccessTokenResponseDto.getAccessToken()); + log.info("카카오 Oauth UserInfo 요청 : {}", httpHeaders); + + ResponseEntity exchange = new RestTemplate().exchange( + kakaoOauthProperties.getUserInfoRequestUrl(), + HttpMethod.GET, + new HttpEntity(httpHeaders), + String.class + ); + log.info("카카오 Oauth UserInfo 응답 : {}", exchange); + + return CustomObjectMapper.readValue(exchange.getBody(), KakaoUserInfoResponseDto.class); + } + +} diff --git a/src/main/java/com/recordit/server/service/oauth/OauthService.java b/src/main/java/com/recordit/server/service/oauth/OauthService.java new file mode 100644 index 00000000..a827efa3 --- /dev/null +++ b/src/main/java/com/recordit/server/service/oauth/OauthService.java @@ -0,0 +1,10 @@ +package com.recordit.server.service.oauth; + +import com.recordit.server.constant.LoginType; + +public interface OauthService { + + LoginType getLoginType(); + + String getUserInfoByOauthToken(String oauthToken); +} diff --git a/src/main/java/com/recordit/server/service/oauth/OauthServiceLocator.java b/src/main/java/com/recordit/server/service/oauth/OauthServiceLocator.java new file mode 100644 index 00000000..f77bf4ad --- /dev/null +++ b/src/main/java/com/recordit/server/service/oauth/OauthServiceLocator.java @@ -0,0 +1,23 @@ +package com.recordit.server.service.oauth; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.exception.member.NotMatchLoginTypeException; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class OauthServiceLocator { + private final List oauthServices; + + public OauthService getOauthServiceByLoginType(LoginType loginType) { + return oauthServices.stream() + .filter(oauthService -> oauthService.getLoginType() == loginType) + .findFirst() + .orElseThrow(() -> new NotMatchLoginTypeException("일치하는 로그인 타입이 없습니다.")); + } +} diff --git a/src/main/java/com/recordit/server/util/CustomObjectMapper.java b/src/main/java/com/recordit/server/util/CustomObjectMapper.java new file mode 100644 index 00000000..177bda96 --- /dev/null +++ b/src/main/java/com/recordit/server/util/CustomObjectMapper.java @@ -0,0 +1,18 @@ +package com.recordit.server.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.NonNull; + +public class CustomObjectMapper { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static T readValue(@NonNull String str, @NonNull Class clazz) { + try { + return objectMapper.readValue(str, clazz); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("잘못된 Json값이나 Class Type을 입력했습니다."); + } + } +} diff --git a/src/main/java/com/recordit/server/util/RedisManager.java b/src/main/java/com/recordit/server/util/RedisManager.java new file mode 100644 index 00000000..79a395e1 --- /dev/null +++ b/src/main/java/com/recordit/server/util/RedisManager.java @@ -0,0 +1,56 @@ +package com.recordit.server.util; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.recordit.server.exception.redis.RedisParsingException; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class RedisManager { + + private static final String REDIS_PARSING_ERROR_MESSAGE = "Redis에서 Json을 파싱할 때 에러가 발생했습니다."; + private final StringRedisTemplate stringRedisTemplate; + private final ObjectMapper objectMapper; + + public void set(@NonNull String key, @NonNull Object value) { + try { + stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException e) { + throw new RedisParsingException(REDIS_PARSING_ERROR_MESSAGE); + } + } + + public void set(@NonNull String key, @NonNull Object value, long timeout, @NonNull TimeUnit timeUnit) { + try { + stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), timeout, timeUnit); + } catch (JsonProcessingException e) { + throw new RedisParsingException(REDIS_PARSING_ERROR_MESSAGE); + } + } + + public void delete(@NonNull String key) { + stringRedisTemplate.delete(key); + } + + public Optional get(@NonNull String key, @NonNull Class clazz) { + String jsonString = stringRedisTemplate.opsForValue().get(key); + if (!StringUtils.hasText(jsonString)) { + return Optional.empty(); + } + try { + return Optional.of(objectMapper.readValue(jsonString, clazz)); + } catch (JsonProcessingException e) { + throw new RedisParsingException(REDIS_PARSING_ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/com/recordit/server/util/S3Uploader.java b/src/main/java/com/recordit/server/util/S3Uploader.java new file mode 100644 index 00000000..c5184dae --- /dev/null +++ b/src/main/java/com/recordit/server/util/S3Uploader.java @@ -0,0 +1,58 @@ +package com.recordit.server.util; + +import java.io.IOException; +import java.util.UUID; + +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +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.recordit.server.environment.S3Properties; +import com.recordit.server.exception.file.FileInputStreamException; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class S3Uploader { + + private final AmazonS3 amazonS3; + private final S3Properties s3Properties; + + public String upload(@NonNull MultipartFile multipartFile) { + String fileName = UUID.randomUUID().toString(); + try { + amazonS3.putObject( + new PutObjectRequest( + s3Properties.getBucket(), + s3Properties.getDirectory() + "/" + fileName, + multipartFile.getInputStream(), + getObjectMetadataBy(multipartFile) + ).withCannedAcl(CannedAccessControlList.PublicRead) + ); + } catch (IOException e) { + throw new FileInputStreamException("해당 파일을 읽어올 수 없습니다."); + } + return fileName; + } + + public void delete(@NonNull String fileName) { + amazonS3.deleteObject(s3Properties.getBucket(), s3Properties.getDirectory() + "/" + fileName); + } + + public String getUrlByFileName(String fileName) { + return amazonS3.getUrl(s3Properties.getBucket(), s3Properties.getDirectory() + "/" + fileName).toString(); + } + + private ObjectMetadata getObjectMetadataBy(@NonNull MultipartFile multipartFile) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(multipartFile.getSize()); + objectMetadata.setContentType(multipartFile.getContentType()); + return objectMetadata; + } + +} diff --git a/src/main/java/com/recordit/server/util/SessionUtil.java b/src/main/java/com/recordit/server/util/SessionUtil.java new file mode 100644 index 00000000..15f3e103 --- /dev/null +++ b/src/main/java/com/recordit/server/util/SessionUtil.java @@ -0,0 +1,37 @@ +package com.recordit.server.util; + +import javax.servlet.http.HttpSession; + +import org.springframework.stereotype.Component; + +import com.recordit.server.exception.member.NotFoundUserInfoInSessionException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class SessionUtil { + + private static final String PREFIX_USER_ID = "LOGIN_USER_ID"; + private final HttpSession httpSession; + + public void saveUserIdInSession(Long id) { + httpSession.setAttribute(PREFIX_USER_ID, id); + } + + public Long findUserIdBySession() { + Object userId = httpSession.getAttribute(PREFIX_USER_ID); + if (userId == null) { + log.info("세션에 사용자 정보가 저장되어 있지 않습니다"); + invalidateSession(); + throw new NotFoundUserInfoInSessionException("세션에 사용자 정보가 저장되어 있지 않습니다"); + } + return Long.valueOf(userId.toString()); + } + + public void invalidateSession() { + httpSession.invalidate(); + } +} \ No newline at end of file diff --git a/src/main/resources/application-datasource.yml b/src/main/resources/application-datasource.yml new file mode 100644 index 00000000..2d8f2b98 --- /dev/null +++ b/src/main/resources/application-datasource.yml @@ -0,0 +1,10 @@ +spring: + config: + activate: + on-profile: "datasource" + datasource: + driver-class-name: org.mariadb.jdbc.Driver + url: jdbc:mariadb://${DATASOURCE_HOST:localhost}:${DATASOURCE_PORT:3306}/RecordIt?characterEncoding=UTF-8&serverTimezone=Asia/Seoul + username: ${DATASOURCE_USERNAME:root} + password: ${DATASOURCE_PASSWORD:} + diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..f59aaf65 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,38 @@ +spring: + config: + activate: + on-profile: "dev" + mvc: + pathmatch: + matching-strategy: ant_path_matcher + jpa: + open-in-view: true + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + properties: + hibernate: + format_sql: true + output: + ansi: + enabled: always + logging: + level: + '[org.springframework.web]': DEBUG + '[org.hibernate]': DEBUG + servlet: + multipart: + max-request-size: 15MB + max-file-size: 5MB +springfox: + documentation: + swagger: + use-model-v3: false + swagger-ui: + base-url: /api +logging: + config: classpath:log4j2/log4j2-dev.xml +cors: + origin: ${CORS_ORIGIN_NAME:} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 00000000..ae66f596 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,38 @@ +spring: + config: + activate: + on-profile: "local" + mvc: + pathmatch: + matching-strategy: ant_path_matcher + jpa: + open-in-view: true + hibernate: + ddl-auto: update + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + show-sql: true + properties: + hibernate: + format_sql: true + output: + ansi: + enabled: always + logging: + level: + '[org.springframework.web]': DEBUG + '[org.hibernate]': DEBUG + servlet: + multipart: + max-request-size: 15MB + max-file-size: 5MB +springfox: + documentation: + swagger: + use-model-v3: false + swagger-ui: + base-url: /api +logging: + config: classpath:log4j2/log4j2-local.xml +cors: + origin: ${CORS_ORIGIN_NAME:} \ No newline at end of file diff --git a/src/main/resources/application-oauth.yml b/src/main/resources/application-oauth.yml new file mode 100644 index 00000000..b65b9b42 --- /dev/null +++ b/src/main/resources/application-oauth.yml @@ -0,0 +1,18 @@ +spring: + config: + activate: + on-profile: "oauth" + +oauth: + kakao: + client-id: ${OAUTH_KAKAO_CLIENT_ID:} + client-secret: ${OAUTH_KAKAO_CLIENT_SECRET:} + redirect-url: ${OAUTH_KAKAO_REDIRECT_URL:} + token-request-url: https://kauth.kakao.com/oauth/token + user-info-request-url: https://kapi.kakao.com/v2/user/me + google: + client-id: ${OAUTH_GOOGLE_CLIENT_ID:} + client-secret: ${OAUTH_GOOGLE_CLIENT_SECRET:} + redirect-url: ${OAUTH_GOOGLE_REDIRECT_URL:} + token-request-url: https://oauth2.googleapis.com/token + user-info-request-url: https://oauth2.googleapis.com/tokeninfo diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..e3e9d668 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,30 @@ +spring: + config: + activate: + on-profile: "prod" + mvc: + pathmatch: + matching-strategy: ant_path_matcher + jpa: + hibernate: + ddl-auto: none + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + servlet: + multipart: + max-request-size: 15MB + max-file-size: 5MB +logging: + config: classpath:log4j2/log4j2-prod.xml +cors: + origin: ${CORS_ORIGIN_NAME:} +springfox: + documentation: + enabled: false + swagger-ui: + enabled: false + open-api: + enabled: false + swagger: + v2: + enabled: false diff --git a/src/main/resources/application-redis.yml b/src/main/resources/application-redis.yml new file mode 100644 index 00000000..a7673504 --- /dev/null +++ b/src/main/resources/application-redis.yml @@ -0,0 +1,11 @@ +spring: + config: + activate: + on-profile: "redis" + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + session: + store-type: redis + diff --git a/src/main/resources/application-s3.yml b/src/main/resources/application-s3.yml new file mode 100644 index 00000000..6c0d3975 --- /dev/null +++ b/src/main/resources/application-s3.yml @@ -0,0 +1,12 @@ +spring: + config: + activate: + on-profile: "s3" + +s3: + credentials: + access-key: ${S3_ACCESS_KEY:} + secret-key: ${S3_SECRET_ACCESS_KEY:} + bucket: ${S3_BUCKET_NAME:} + directory: ${S3_DIRECTORY_NAME:} + region: ap-northeast-2 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b137891..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..9ea40c73 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + profiles: + group: + "test": "test" + "local": "local, datasource, redis, oauth, s3" + "dev": "dev, datasource, redis, oauth, s3" + "prod": "prod, datasource, redis, oauth, s3" diff --git a/src/main/resources/log4j2/log4j2-dev.xml b/src/main/resources/log4j2/log4j2-dev.xml new file mode 100644 index 00000000..f648de7c --- /dev/null +++ b/src/main/resources/log4j2/log4j2-dev.xml @@ -0,0 +1,45 @@ + + + + ./logs + recordIt + + + + + + + + + ${LOGS_PATH}/${LOGS_NAME}.log + ${LOGS_PATH}/${LOGS_NAME}.%d{yyyy-MM-dd}.%i.log + + %d{yyyy-MM-dd HH:mm:ss} %5p [%equals{%X{trace_id}}{}{system}] [%c] %m%n + + + + + + + + ${LOGS_PATH}/ERROR/${LOGS_NAME}_ERROR.log + ${LOGS_PATH}/ERROR/${LOGS_NAME}_ERROR.%d{yyyy-MM-dd}.%i.log + + %d{yyyy-MM-dd HH:mm:ss} %5p [%equals{%X{trace_id}}{}{system}] [%c] %m%n + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/log4j2/log4j2-local.xml b/src/main/resources/log4j2/log4j2-local.xml new file mode 100644 index 00000000..6ffa7132 --- /dev/null +++ b/src/main/resources/log4j2/log4j2-local.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/log4j2/log4j2-prod.xml b/src/main/resources/log4j2/log4j2-prod.xml new file mode 100644 index 00000000..f648de7c --- /dev/null +++ b/src/main/resources/log4j2/log4j2-prod.xml @@ -0,0 +1,45 @@ + + + + ./logs + recordIt + + + + + + + + + ${LOGS_PATH}/${LOGS_NAME}.log + ${LOGS_PATH}/${LOGS_NAME}.%d{yyyy-MM-dd}.%i.log + + %d{yyyy-MM-dd HH:mm:ss} %5p [%equals{%X{trace_id}}{}{system}] [%c] %m%n + + + + + + + + ${LOGS_PATH}/ERROR/${LOGS_NAME}_ERROR.log + ${LOGS_PATH}/ERROR/${LOGS_NAME}_ERROR.%d{yyyy-MM-dd}.%i.log + + %d{yyyy-MM-dd HH:mm:ss} %5p [%equals{%X{trace_id}}{}{system}] [%c] %m%n + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/recodeit/server/RecodeItServerApplicationTests.java b/src/test/java/com/recodeit/server/RecodeItServerApplicationTests.java deleted file mode 100644 index d836da3a..00000000 --- a/src/test/java/com/recodeit/server/RecodeItServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.recodeit.server; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class RecodeItServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/recordit/server/RecordItServerApplicationTests.java b/src/test/java/com/recordit/server/RecordItServerApplicationTests.java new file mode 100644 index 00000000..dfc23898 --- /dev/null +++ b/src/test/java/com/recordit/server/RecordItServerApplicationTests.java @@ -0,0 +1,15 @@ +package com.recordit.server; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class RecordItServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/recordit/server/configuration/EmbeddedRedisConfiguration.java b/src/test/java/com/recordit/server/configuration/EmbeddedRedisConfiguration.java new file mode 100644 index 00000000..3d8ad624 --- /dev/null +++ b/src/test/java/com/recordit/server/configuration/EmbeddedRedisConfiguration.java @@ -0,0 +1,28 @@ +package com.recordit.server.configuration; + +import javax.annotation.PreDestroy; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import redis.embedded.RedisServer; + +@Configuration +@Profile("test") +public class EmbeddedRedisConfiguration { + + private final RedisServer redisServer; + + protected EmbeddedRedisConfiguration(@Value("${spring.redis.port}") int redisPort) { + this.redisServer = new RedisServer(redisPort); + redisServer.start(); + } + + @PreDestroy + private void stop() { + if (redisServer != null) { + redisServer.stop(); + } + } +} diff --git a/src/test/java/com/recordit/server/service/CommentServiceTest.java b/src/test/java/com/recordit/server/service/CommentServiceTest.java new file mode 100644 index 00000000..a77c0d27 --- /dev/null +++ b/src/test/java/com/recordit/server/service/CommentServiceTest.java @@ -0,0 +1,265 @@ +package com.recordit.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.domain.Comment; +import com.recordit.server.domain.Record; +import com.recordit.server.dto.comment.CommentRequestDto; +import com.recordit.server.dto.comment.WriteCommentRequestDto; +import com.recordit.server.dto.comment.WriteCommentResponseDto; +import com.recordit.server.exception.comment.CommentNotFoundException; +import com.recordit.server.exception.comment.EmptyContentException; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.member.NotFoundUserInfoInSessionException; +import com.recordit.server.exception.record.RecordNotFoundException; +import com.recordit.server.repository.CommentRepository; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.repository.RecordRepository; +import com.recordit.server.util.SessionUtil; + +@ExtendWith(MockitoExtension.class) +public class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + + @Mock + private ImageFileService imageFileService; + + @Mock + private CommentRepository commentRepository; + + @Mock + private MemberRepository memberRepository; + + @Mock + private RecordRepository recordRepository; + + @Mock + private SessionUtil sessionUtil; + + @Nested + @DisplayName("댓글을 작성 할 때") + class 댓글을_작성_할_때 { + + @Test + @DisplayName("내용과 첨부파일이 모두 비어있다면 예외를 던진다") + void 내용과_첨부파일이_모두_비어있다면_예외를_던진다() { + // given + WriteCommentRequestDto writeCommentRequestDto = WriteCommentRequestDto.builder() + .recordId(1L) + .parentId(1L) + .comment("") + .build(); + MockMultipartFile multipartFile = mock(MockMultipartFile.class); + + given(imageFileService.isEmptyFile(any(MultipartFile.class))) + .willReturn(true); + + // when, then + assertThatThrownBy(() -> commentService.writeComment(writeCommentRequestDto, multipartFile)) + .isInstanceOf(EmptyContentException.class); + } + + @Test + @DisplayName("세션에 저장된 사용자가 DB에 존재하지 않으면 예외를 던진다") + void 세션에_저장된_사용자가_DB에_존재하지_않으면_예외를_던진다() { + // given + WriteCommentRequestDto writeCommentRequestDto = WriteCommentRequestDto.builder() + .recordId(1L) + .parentId(1L) + .comment("test") + .build(); + MockMultipartFile multipartFile = mock(MockMultipartFile.class); + + given(sessionUtil.findUserIdBySession()) + .willReturn(1L); + given(memberRepository.findById(1L)) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> commentService.writeComment(writeCommentRequestDto, multipartFile)) + .isInstanceOf(MemberNotFoundException.class); + + } + + @Test + @DisplayName("지정한 레코드가 존재하지 않으면 예외를 던진다") + void 지정한_레코드가_존재하지_않으면_예외를_던진다() { + // given + WriteCommentRequestDto writeCommentRequestDto = WriteCommentRequestDto.builder() + .recordId(1L) + .parentId(1L) + .comment("test") + .build(); + MockMultipartFile multipartFile = mock(MockMultipartFile.class); + + given(sessionUtil.findUserIdBySession()) + .willThrow(new NotFoundUserInfoInSessionException("세션에 사용자 정보가 저장되어 있지 않습니다")); + given(recordRepository.findById(writeCommentRequestDto.getRecordId())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> commentService.writeComment(writeCommentRequestDto, multipartFile)) + .isInstanceOf(RecordNotFoundException.class); + } + + @Test + @DisplayName("지정한 부모 댓글이 존재하지 않으면 예외를 던진다") + void 지정한_부모_댓글이_존재하지_않으면_예외를_던진다() { + // given + WriteCommentRequestDto writeCommentRequestDto = WriteCommentRequestDto.builder() + .recordId(1L) + .parentId(1L) + .comment("test") + .build(); + MockMultipartFile multipartFile = mock(MockMultipartFile.class); + Record record = mock(Record.class); + + given(sessionUtil.findUserIdBySession()) + .willThrow(new NotFoundUserInfoInSessionException("세션에 사용자 정보가 저장되어 있지 않습니다")); + given(recordRepository.findById(writeCommentRequestDto.getRecordId())) + .willReturn(Optional.of(record)); + given(commentRepository.findById(writeCommentRequestDto.getParentId())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> commentService.writeComment(writeCommentRequestDto, multipartFile)) + .isInstanceOf(CommentNotFoundException.class); + } + + @Test + @DisplayName("정상적으로 작성이 완료되면 ID를 반환한다") + void 정상적으로_작성이_완료되면_ID를_반환한다() { + // given + WriteCommentRequestDto writeCommentRequestDto = WriteCommentRequestDto.builder() + .recordId(1L) + .parentId(1L) + .comment("test") + .build(); + MockMultipartFile multipartFile = mock(MockMultipartFile.class); + Record record = mock(Record.class); + Comment parentComment = mock(Comment.class); + Comment saveComment = mock(Comment.class); + + given(sessionUtil.findUserIdBySession()) + .willThrow(new NotFoundUserInfoInSessionException("세션에 사용자 정보가 저장되어 있지 않습니다")); + given(recordRepository.findById(writeCommentRequestDto.getRecordId())) + .willReturn(Optional.of(record)); + given(commentRepository.findById(writeCommentRequestDto.getParentId())) + .willReturn(Optional.of(parentComment)); + given(commentRepository.save(any())) + .willReturn(saveComment); + given(saveComment.getId()) + .willReturn(2L); + + // when + WriteCommentResponseDto result = commentService.writeComment( + writeCommentRequestDto, + multipartFile + ); + + // then + assertThat(result.getCommentId()).isEqualTo(2L); + assertThatCode(() -> commentService.writeComment( + writeCommentRequestDto, + multipartFile + )).doesNotThrowAnyException(); + } + } + + @Nested + @DisplayName("댓글을 조회시") + class 댓글을_조회_할_때 { + + private CommentRequestDto commentRequestDto = mock(CommentRequestDto.class); + + @BeforeEach + void init() { + given(commentRequestDto.getPage()) + .willReturn(0); + given(commentRequestDto.getSize()) + .willReturn(1); + } + + @Nested + @DisplayName("조회하려는 부모 댓글 ID가 null일 때") + class 조회하려는_부모_댓글_ID가_null일_때 { + + @BeforeEach + void init() { + given(commentRequestDto.getParentId()) + .willReturn(null); + } + + @Test + @DisplayName("지정한 레코드 ID가 존재하지 않으면 예외를 던진다") + void 지정한_레코드_ID가_존재하지_않으면_예외를_던진다() { + // given + given(recordRepository.findById(any())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> commentService.getCommentsBy(commentRequestDto)) + .isInstanceOf(RecordNotFoundException.class); + } + + } + + @Nested + @DisplayName("조회하려는 부모 댓글 ID가 null이 아닐 때") + class 조회하려는_부모_댓글_ID가_null이_아닐_때 { + + @BeforeEach + void init() { + given(commentRequestDto.getParentId()) + .willReturn(1L); + } + + @Test + @DisplayName("지정한 부모 댓글이 존재하지 않으면 예외를 던진다") + void 지정한_부모_댓글이_존재하지_않으면_예외를_던진다() { + // given + given(commentRepository.findById(commentRequestDto.getParentId())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> commentService.getCommentsBy(commentRequestDto)) + .isInstanceOf(CommentNotFoundException.class); + } + + @Test + @DisplayName("부모가 부모를 가질 때 예외를 던진다") + void 부모가_부모를_가질_때_예외를_던진다() { + // given + Comment parentComment = mock(Comment.class); + Comment grandParentComment = mock(Comment.class); + given(commentRepository.findById(commentRequestDto.getParentId())) + .willReturn(Optional.of(parentComment)); + given(parentComment.getParentComment()) + .willReturn(grandParentComment); + + // when, then + assertThatThrownBy(() -> commentService.getCommentsBy(commentRequestDto)) + .isInstanceOf(IllegalStateException.class); + } + + } + + } + +} diff --git a/src/test/java/com/recordit/server/service/ImageFileServiceTest.java b/src/test/java/com/recordit/server/service/ImageFileServiceTest.java new file mode 100644 index 00000000..372387e3 --- /dev/null +++ b/src/test/java/com/recordit/server/service/ImageFileServiceTest.java @@ -0,0 +1,117 @@ +package com.recordit.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.constant.RefType; +import com.recordit.server.exception.file.FileContentTypeNotAllowedException; +import com.recordit.server.exception.file.FileExtensionNotAllowedException; +import com.recordit.server.repository.ImageFileRepository; +import com.recordit.server.util.S3Uploader; + +@ExtendWith(MockitoExtension.class) +class ImageFileServiceTest { + + @InjectMocks + private ImageFileService imageFileService; + + @Mock + private ImageFileRepository imageFileRepository; + + @Mock + private S3Uploader s3Uploader; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + @Nested + @DisplayName("첨부 파일을 저장할 때") + class 첨부_파일을_저장할_때 { + + private final RefType refType = Arrays.stream(RefType.values()).findAny().get(); + private final Long refId = 0L; + private final MultipartFile mock = mock(MultipartFile.class); + private final List mockMultipartFiles = List.of(mock); + + @Test + @DisplayName("빈 파일이 넘어오면 null을 응답한다") + void 빈_파일이_넘어오면_예외를_던진다() { + // given + given(mock.isEmpty()) + .willReturn(true); + + // when + List result = imageFileService.saveAttachmentFiles(refType, refId, mockMultipartFiles); + + // then + assertThat(result.isEmpty()).isTrue(); + } + + @Test + @DisplayName("규정한 ContentType이 아니면 예외를 던진다") + void 규정한_ContentType이_아니면_예외를_던진다() { + // given + given(mock.isEmpty()) + .willReturn(false); + given(mock.getContentType()) + .willReturn("notImage"); + + // when, then + assertThatThrownBy(() -> imageFileService.saveAttachmentFiles(refType, refId, mockMultipartFiles)) + .isInstanceOf(FileContentTypeNotAllowedException.class); + } + + @Test + @DisplayName("규정한 이미지 파일 확장자가 아니면 예외를 던진다") + void 규정한_이미지_파일_확장자가_아니면_예외를_던진다() { + // given + given(mock.isEmpty()) + .willReturn(false); + given(mock.getContentType()) + .willReturn("image/jpg"); + given(mock.getOriginalFilename()) + .willReturn("test.xlsx"); + + // when, then + assertThatThrownBy(() -> imageFileService.saveAttachmentFiles(refType, refId, mockMultipartFiles)) + .isInstanceOf(FileExtensionNotAllowedException.class); + } + + @Test + @DisplayName("정상적으로 파일을 저장할 경우 Url을 응답한다") + void 정상적으로_파일을_저장할_경우_URL을_응답한다() { + // given + given(mock.isEmpty()) + .willReturn(false); + given(mock.getContentType()) + .willReturn("image"); + given(s3Uploader.upload(any())) + .willReturn("saveFileName"); + given(s3Uploader.getUrlByFileName(any())) + .willReturn("saveFileUrl"); + given(mock.getOriginalFilename()) + .willReturn("test.png"); + + // when + List result = imageFileService.saveAttachmentFiles(refType, refId, mockMultipartFiles); + + // then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo("saveFileUrl"); + } + + } +} \ No newline at end of file diff --git a/src/test/java/com/recordit/server/service/MemberServiceTest.java b/src/test/java/com/recordit/server/service/MemberServiceTest.java new file mode 100644 index 00000000..1c5f51f7 --- /dev/null +++ b/src/test/java/com/recordit/server/service/MemberServiceTest.java @@ -0,0 +1,266 @@ +package com.recordit.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.domain.Member; +import com.recordit.server.dto.member.LoginRequestDto; +import com.recordit.server.dto.member.RegisterRequestDto; +import com.recordit.server.dto.member.RegisterSessionResponseDto; +import com.recordit.server.exception.member.DuplicateNicknameException; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.member.NotFoundRegisterSessionException; +import com.recordit.server.exception.member.NotFoundUserInfoInSessionException; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.service.oauth.OauthService; +import com.recordit.server.service.oauth.OauthServiceLocator; +import com.recordit.server.util.RedisManager; +import com.recordit.server.util.SessionUtil; + +@ExtendWith(MockitoExtension.class) +public class MemberServiceTest { + + @InjectMocks + private MemberService memberService; + + @Mock + private OauthServiceLocator oauthServiceLocator; + + @Mock + private MemberRepository memberRepository; + + @Mock + private SessionUtil sessionUtil; + + @Mock + private RedisManager redisManager; + + @Mock + private Member mockMember; + + private MockedStatic mockUUID; + + private final LoginType loginType = Arrays.stream(LoginType.values()) + .findFirst() + .get(); + + private final UUID testUUID = UUID.randomUUID(); + + private final String mockOauthId = "testOauthId"; + private final String nickname = "testNickname"; + + @BeforeEach + void init() { + mockUUID = mockStatic(UUID.class); + } + + @AfterEach + void afterEach() { + mockUUID.close(); + } + + @Nested + @DisplayName("oauth 로그인을 할 때") + class oauth_로그인을_할_때_oauthToken을_통해_찾은_사용자가 { + + @Nested + @DisplayName("oauthToken을 통해 찾은 사용자가") + class oauthToken을_통해_찾은_사용자가 { + + @Mock + private OauthService oauthService; + + private final String mockOauthToken = "testOauthToken"; + + private final LoginRequestDto loginRequestDto = LoginRequestDto.builder() + .oauthToken(mockOauthToken) + .build(); + + @Test + @DisplayName("있으면 null을 반환한다") + void 있으면_null을_반환한다() { + // given + Optional mockMember = Optional.of(MemberServiceTest.this.mockMember); + + given(oauthServiceLocator.getOauthServiceByLoginType(any())) + .willReturn(oauthService); + given(oauthService.getUserInfoByOauthToken(anyString())) + .willReturn(mockOauthId); + given(memberRepository.findByOauthId(mockOauthId)) + .willReturn(mockMember); + + // when + Optional result = memberService.oauthLogin( + loginType, + loginRequestDto + ); + + // then + assertThat(result.isEmpty()).isTrue(); + } + + @Test + @DisplayName("없으면 registerSessionUUID를 반환한다") + void 없으면_registerSessionUUID를_반환한다() { + // given + given(oauthServiceLocator.getOauthServiceByLoginType(any())) + .willReturn(oauthService); + given(oauthService.getUserInfoByOauthToken(anyString())) + .willReturn(mockOauthId); + given(memberRepository.findByOauthId(mockOauthId)) + .willReturn(Optional.empty()); + given(UUID.randomUUID()) + .willReturn(testUUID); + + // when + Optional result = memberService.oauthLogin( + loginType, + loginRequestDto + ); + + // then + assertThat(result.isPresent()).isTrue(); + assertThat(result.get().getRegisterSession()).isEqualTo(testUUID.toString()); + } + } + } + + @Nested + @DisplayName("oauth 회원가입을 할 때") + class oauth_회원가입을_할_때 { + + @Nested + @DisplayName("Redis에 RegisterSession이") + class Redis에_RegisterSession이 { + + private RegisterRequestDto registerRequestDto = RegisterRequestDto.builder() + .registerSession(testUUID.toString()) + .nickname(nickname) + .build(); + + @Test + @DisplayName("없으면 예외를 던진다") + void 없으면_예외를_던진다() { + // given + given(redisManager.get(anyString(), any())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> memberService.oauthRegister( + loginType, + registerRequestDto + )).isInstanceOf(NotFoundRegisterSessionException.class); + + } + + @Test + @DisplayName("있으면 예외를 던지지 않는다") + void 있으면_예외를_던지지_않는다() { + // given + given(redisManager.get(anyString(), any())) + .willReturn(Optional.of(mockOauthId)); + given(memberRepository.existsByNickname(anyString())) + .willReturn(false); + given(memberRepository.save(any())).willReturn(mockMember); + willDoNothing().given(sessionUtil).saveUserIdInSession(anyLong()); + + // when, then + assertThatCode(() -> memberService.oauthRegister(loginType, registerRequestDto)) + .doesNotThrowAnyException(); + } + + } + } + + @Nested + @DisplayName("닉네임 중복 확인 기능에서") + class 닉네임_중복_확인_기능에서_닉네임이 { + + @Test + @DisplayName("중복되면 예외를 던진다") + void 중복되면_예외를_던진다() { + // given + given(memberRepository.existsByNickname(anyString())) + .willReturn(true); + + // when, then + assertThatThrownBy(() -> memberService.isDuplicateNickname(nickname)) + .isInstanceOf(DuplicateNicknameException.class); + } + + @Test + @DisplayName("중복되지 않으면 예외를 던지지 않는다") + void 중복되지_않으면_예외를_던지지_않는다() { + // given + given(memberRepository.existsByNickname(anyString())) + .willReturn(false); + + // when, then + assertThatCode(() -> memberService.isDuplicateNickname(nickname)) + .doesNotThrowAnyException(); + } + } + + @Nested + @DisplayName("로그인 된 사용자의 닉네임을 응답하는 기능에서") + class 로그인_된_사용자의_닉네임을_응답하는_기능에서 { + + @Test + @DisplayName("로그인이 되어있지 않다면 예외를 던진다") + void 로그인이_되어있지_않으면_예외를_던진다() { + // given + given(sessionUtil.findUserIdBySession()) + .willThrow(new NotFoundUserInfoInSessionException("세션에 사용자 정보가 저장되어 있지 않습니다")); + + // when, then + assertThatThrownBy(() -> memberService.findNicknameIfPresent()) + .isInstanceOf(NotFoundUserInfoInSessionException.class); + } + + @Test + @DisplayName("세션에 회원의 아이디는 존재하지만 테이블에 없다면 예외를 던진다") + void 세션에_회원의_아이디는_존재하지만_테이블에_없다면_예외를_던진다() { + // given + given(sessionUtil.findUserIdBySession()) + .willReturn(1L); + given(memberRepository.findById(any())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> memberService.findNicknameIfPresent()) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + @DisplayName("정상적으로 로그인되어있고 테이블에도 정보가 있는 경우엔 예외를 던지지 않는다") + void 정상적으로_로그인되어있고_테이블에도_정보가_있는_경우엔_예외를_던지지_않는다() { + //given + given(sessionUtil.findUserIdBySession()) + .willReturn(1L); + given(memberRepository.findById(any())) + .willReturn(Optional.of(mockMember)); + given(mockMember.getNickname()) + .willReturn(nickname); + + // when, then + assertThatCode(() -> memberService.findNicknameIfPresent()) + .doesNotThrowAnyException(); + } + } +} diff --git a/src/test/java/com/recordit/server/service/RecordCategoryServiceTest.java b/src/test/java/com/recordit/server/service/RecordCategoryServiceTest.java new file mode 100644 index 00000000..d109e02e --- /dev/null +++ b/src/test/java/com/recordit/server/service/RecordCategoryServiceTest.java @@ -0,0 +1,65 @@ +package com.recordit.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.recordit.server.domain.RecordCategory; +import com.recordit.server.dto.record.category.RecordCategoryResponseDto; +import com.recordit.server.repository.RecordCategoryRepository; + +@ExtendWith({MockitoExtension.class}) +class RecordCategoryServiceTest { + + @InjectMocks + private RecordCategoryService recordCategoryService; + + @Mock + private RecordCategoryRepository recordCategoryRepository; + + @Test + @DisplayName("레코드 카테고리 전체 조회를 테스트한다") + void 레코드_카테고리_전체_조회를_테스트한다() { + // given + RecordCategory recordCategory1 = mock(RecordCategory.class); + RecordCategory recordCategory2 = mock(RecordCategory.class); + RecordCategory recordCategory3 = mock(RecordCategory.class); + + given(recordCategory1.getId()) + .willReturn(1L); + given(recordCategory1.getName()) + .willReturn("recordCategory1"); + + given(recordCategory2.getId()) + .willReturn(2L); + given(recordCategory2.getName()) + .willReturn("recordCategory2"); + given(recordCategory2.getParentRecordCategory()) + .willReturn(recordCategory1); + + given(recordCategory3.getId()) + .willReturn(3L); + given(recordCategory3.getName()) + .willReturn("recordCategory3"); + + given(recordCategoryRepository.findAllFetchDepthIsOne()) + .willReturn(List.of(recordCategory1, recordCategory2, recordCategory3)); + // when + List result = recordCategoryService.getAllRecordCategories(); + + // then + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(0).getSubcategories().size()).isEqualTo(1); + assertThat(result.get(0).getSubcategories().get(0).getId()).isEqualTo(2L); + assertThat(result.get(1).getId()).isEqualTo(3L); + } + +} \ No newline at end of file diff --git a/src/test/java/com/recordit/server/service/RecordServiceTest.java b/src/test/java/com/recordit/server/service/RecordServiceTest.java new file mode 100644 index 00000000..14886fe4 --- /dev/null +++ b/src/test/java/com/recordit/server/service/RecordServiceTest.java @@ -0,0 +1,230 @@ +package com.recordit.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.multipart.MultipartFile; + +import com.recordit.server.domain.Member; +import com.recordit.server.domain.Record; +import com.recordit.server.domain.RecordCategory; +import com.recordit.server.domain.RecordColor; +import com.recordit.server.domain.RecordIcon; +import com.recordit.server.dto.record.WriteRecordRequestDto; +import com.recordit.server.exception.member.MemberNotFoundException; +import com.recordit.server.exception.record.RecordColorNotFoundException; +import com.recordit.server.exception.record.RecordIconNotFoundException; +import com.recordit.server.exception.record.RecordNotFoundException; +import com.recordit.server.exception.record.category.RecordCategoryNotFoundException; +import com.recordit.server.repository.ImageFileRepository; +import com.recordit.server.repository.MemberRepository; +import com.recordit.server.repository.RecordCategoryRepository; +import com.recordit.server.repository.RecordColorRepository; +import com.recordit.server.repository.RecordIconRepository; +import com.recordit.server.repository.RecordRepository; +import com.recordit.server.util.SessionUtil; + +@ExtendWith(MockitoExtension.class) +@ActiveProfiles("test") +class RecordServiceTest { + @InjectMocks + private RecordService recordService; + + @Mock + private ImageFileRepository imageFileRepository; + + @Mock + private ImageFileService imageFileService; + + @Mock + private SessionUtil sessionUtil; + + @Mock + private MemberRepository memberRepository; + + @Mock + private RecordCategoryRepository recordCategoryRepository; + + @Mock + private RecordColorRepository recordColorRepository; + + @Mock + private RecordIconRepository recordIconRepository; + + @Mock + private RecordRepository recordRepository; + + @Mock + private Member mockMember; + + @Mock + private RecordCategory mockRecordCategory; + + @Mock + private RecordColor mockRecordColor; + + @Mock + private RecordIcon mockRecordIcon; + + @Mock + private Record mockRecord; + + @Nested + @DisplayName("레코드를 작성 할 때") + class 레코드를_작성_할_때 { + private final Long recordCategoryId = 10L; + private final String title = "오늘 내 생일이야!"; + private final String content = "오늘은 내 20번째 생일입니다. \n모두 축하와 선물을 준비해 주세요."; + private final String colorName = "icon-purple"; + private final String iconName = "moon"; + private List files = List.of(); + + private final WriteRecordRequestDto writeRecordRequestDto = WriteRecordRequestDto.builder() + .recordCategoryId(recordCategoryId) + .title(title) + .content(content) + .colorName(colorName) + .iconName(iconName) + .build(); + + @Test + @DisplayName("회원_정보를_찾을 수 없다면 예외를 던진다") + void 회원_정보를_찾을_수_없다면_예외를_던진다() { + // given + given(memberRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> recordService.writeRecord(writeRecordRequestDto, files)) + .isInstanceOf(MemberNotFoundException.class) + .hasMessage("회원 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("카테고리_정보를_찾을 수 없다면 예외를 던진다") + void 카테고리_정보를_찾을_수_없다면_예외를_던진다() { + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(mockMember)); + given(recordCategoryRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> recordService.writeRecord(writeRecordRequestDto, files)) + .isInstanceOf(RecordCategoryNotFoundException.class) + .hasMessage("카테고리 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("컬러_정보를_찾을 수 없다면 예외를 던진다") + void 컬러_정보를_찾을_수_없다면_예외를_던진다() { + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(mockMember)); + given(recordCategoryRepository.findById(anyLong())) + .willReturn(Optional.of(mockRecordCategory)); + given(recordColorRepository.findByName(anyString())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> recordService.writeRecord(writeRecordRequestDto, files)) + .isInstanceOf(RecordColorNotFoundException.class) + .hasMessage("컬러 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("아이콘 정보를 찾을 수 없다면 예외를 던진다") + void 아이콘_정보를_찾을_수_없다면_예외를_던진다() { + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(mockMember)); + given(recordCategoryRepository.findById(anyLong())) + .willReturn(Optional.of(mockRecordCategory)); + given(recordColorRepository.findByName(anyString())) + .willReturn(Optional.of(mockRecordColor)); + given(recordIconRepository.findByName(anyString())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> recordService.writeRecord(writeRecordRequestDto, files)) + .isInstanceOf(RecordIconNotFoundException.class) + .hasMessage("아이콘 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("입력_정보가_올바르다면 예외를 던지지 않는다") + void 입력_정보가_올바르다면_예외를_던지지_않는다() { + // given + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(mockMember)); + given(recordCategoryRepository.findById(anyLong())) + .willReturn(Optional.of(mockRecordCategory)); + given(recordColorRepository.findByName(anyString())) + .willReturn(Optional.of(mockRecordColor)); + given(recordIconRepository.findByName(anyString())) + .willReturn(Optional.of(mockRecordIcon)); + + given(recordRepository.save(any())).willReturn(mockRecord); + given(mockRecord.getId()).willReturn(2394L); + + // when, then + assertThatCode(() -> recordService.writeRecord(writeRecordRequestDto, files)) + .doesNotThrowAnyException(); + } + } + + @Nested + @DisplayName("레코드를 단건 조회 할 때") + class 레코드를_단건_조회_할_때 { + @Test + @DisplayName("레코드 정보를 찾을 수 없다면 예외를 던진다") + void 레코드_정보를_찾을_수_없다면_예외를_던진다() { + // given + given(recordRepository.findById(anyLong())) + .willReturn(Optional.empty()); + + // when, then + assertThatThrownBy(() -> recordService.getDetailRecord(234L)) + .isInstanceOf(RecordNotFoundException.class) + .hasMessage("레코드 정보를 찾을 수 없습니다."); + } + + @Test + @DisplayName("레코드 정보를 찾을 수 있다면 예외를 던지지 않는다") + void 레코드_정보를_찾을_수_있다면_예외를_던지지_않는다() { + // given + Long recordId = 5421L; + Long recordCategotyId = 5L; + + given(mockRecord.getId()).willReturn(recordId); + + given(mockRecord.getRecordCategory()).willReturn(mockRecordCategory); + given(mockRecordCategory.getId()).willReturn(recordCategotyId); + given(mockRecordCategory.getName()).willReturn("축하해주세요"); + + given(mockRecord.getWriter()).willReturn(mockMember); + given(mockMember.getNickname()).willReturn("히니"); + + given(mockRecord.getRecordColor()).willReturn(mockRecordColor); + given(mockRecordColor.getName()).willReturn("icon-pink"); + + given(mockRecord.getRecordIcon()).willReturn(mockRecordIcon); + given(mockRecordIcon.getName()).willReturn("umbrella"); + + given(recordRepository.findById(anyLong())) + .willReturn(Optional.of(mockRecord)); + + // when, then + assertThatCode(() -> recordService.getDetailRecord(recordId)) + .doesNotThrowAnyException(); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/recordit/server/service/oauth/GoogleOauthServiceTest.java b/src/test/java/com/recordit/server/service/oauth/GoogleOauthServiceTest.java new file mode 100644 index 00000000..a34b4841 --- /dev/null +++ b/src/test/java/com/recordit/server/service/oauth/GoogleOauthServiceTest.java @@ -0,0 +1,35 @@ +package com.recordit.server.service.oauth; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.environment.GoogleOauthProperties; + +@ExtendWith(MockitoExtension.class) +public class GoogleOauthServiceTest { + + @InjectMocks + private GoogleOauthService googleOauthService; + + @Mock + private GoogleOauthProperties googleOauthProperties; + + @Test + @DisplayName("LoginType이 정상적으로 응답되는지 테스트한다") + void LoginType이_정상적으로_응답되는지_테스트한다() { + // given + + // when + LoginType loginType = googleOauthService.getLoginType(); + + // then + assertThat(loginType).isEqualTo(LoginType.GOOGLE); + } +} diff --git a/src/test/java/com/recordit/server/service/oauth/KakaoOauthServiceTest.java b/src/test/java/com/recordit/server/service/oauth/KakaoOauthServiceTest.java new file mode 100644 index 00000000..26e15c66 --- /dev/null +++ b/src/test/java/com/recordit/server/service/oauth/KakaoOauthServiceTest.java @@ -0,0 +1,35 @@ +package com.recordit.server.service.oauth; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.recordit.server.constant.LoginType; +import com.recordit.server.environment.KakaoOauthProperties; + +@ExtendWith(MockitoExtension.class) +class KakaoOauthServiceTest { + + @InjectMocks + private KakaoOauthService kakaoOauthService; + + @Mock + private KakaoOauthProperties kakaoOauthProperties; + + @Test + @DisplayName("LoginType이 정상적으로 응답되는지 테스트한다") + void LoginType이_정상적으로_응답되는지_테스트한다() { + // given + + // when + LoginType loginType = kakaoOauthService.getLoginType(); + + // then + assertThat(loginType).isEqualTo(LoginType.KAKAO); + } +} \ No newline at end of file diff --git a/src/test/java/com/recordit/server/util/CustomObjectMapperTest.java b/src/test/java/com/recordit/server/util/CustomObjectMapperTest.java new file mode 100644 index 00000000..40a3409c --- /dev/null +++ b/src/test/java/com/recordit/server/util/CustomObjectMapperTest.java @@ -0,0 +1,86 @@ +package com.recordit.server.util; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Objects; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class CustomObjectMapperTest { + + @Nested + @DisplayName("입력된 Json 값이") + class 입력된_Json_값이 { + + @Test + @DisplayName("잘못된 경우 예외를 던진다") + void 잘못된_경우_예외를_던진다() { + // given + String str = "test"; + Class anyClassType = Object.class; + + // when, then + assertThatThrownBy(() -> CustomObjectMapper.readValue(str, anyClassType)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("정상적인 경우 예외를 던지지 않는다") + void 정상적인_경우_예외를_던지지_않는다() throws Exception { + // given + MappingDummy mappingDummy = new MappingDummy("testField1", "testField2"); + String mappingDummyToJsonString = "{\"field1\":\"testField1\",\"field2\":\"testField2\"}"; + Class mappingDummyClass = MappingDummy.class; + + // when, then + assertThatCode(() -> CustomObjectMapper.readValue(mappingDummyToJsonString, mappingDummyClass)) + .doesNotThrowAnyException(); + assertThat(mappingDummy).isEqualTo( + CustomObjectMapper.readValue(mappingDummyToJsonString, mappingDummyClass)); + + } + } + +} + +class MappingDummy { + private String field1; + private String field2; + + public MappingDummy(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } + + public MappingDummy() { + } + + public String getField1() { + return field1; + } + + public String getField2() { + return field2; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + MappingDummy mappingDummy = (MappingDummy)o; + return Objects.equals(getField1(), mappingDummy.getField1()) && Objects.equals(getField2(), + mappingDummy.getField2()); + } + + @Override + public int hashCode() { + return Objects.hash(getField1(), getField2()); + } +} diff --git a/src/test/java/com/recordit/server/util/S3UploaderTest.java b/src/test/java/com/recordit/server/util/S3UploaderTest.java new file mode 100644 index 00000000..c9d00a25 --- /dev/null +++ b/src/test/java/com/recordit/server/util/S3UploaderTest.java @@ -0,0 +1,89 @@ +package com.recordit.server.util; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.io.IOException; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; + +import com.amazonaws.services.s3.AmazonS3; +import com.recordit.server.environment.S3Properties; +import com.recordit.server.exception.file.FileInputStreamException; + +@ExtendWith(MockitoExtension.class) +class S3UploaderTest { + + @InjectMocks + private S3Uploader s3Uploader; + + @Mock + private AmazonS3 amazonS3; + + @Mock + private S3Properties s3Properties; + + @Mock + private MockMultipartFile multipartFile; + + private MockedStatic mockUUID; + + private final UUID testUUID = UUID.randomUUID(); + + @BeforeEach + void init() { + mockUUID = mockStatic(UUID.class); + given(s3Properties.getBucket()) + .willReturn("testBucket"); + } + + @AfterEach + void afterEach() { + mockUUID.close(); + } + + @Nested + @DisplayName("파일을 업로드할 때") + class 파일을_업로드할_때 { + + @Test + @DisplayName("MultipartFile을 읽어올 수 없을 경우 예외를 던진다") + void MultipartFile을_읽어올_수_없을_경우_예외를_던진다() throws Exception { + // given + given(UUID.randomUUID()) + .willReturn(testUUID); + given(multipartFile.getInputStream()) + .willThrow(IOException.class); + + // when, then + assertThatThrownBy(() -> s3Uploader.upload(multipartFile)) + .isInstanceOf(FileInputStreamException.class); + } + + @Test + @DisplayName("정상적으로 읽어올 수 있을 경우 예외를 던지지 않는다") + void 정상적으로_읽어올_수_있을_경우_예외를_던지지_않는다() { + // given + given(UUID.randomUUID()) + .willReturn(testUUID); + + // when + String fileName = s3Uploader.upload(multipartFile); + + // then + assertThat(fileName).isEqualTo(testUUID.toString()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/recordit/server/util/SessionUtilTest.java b/src/test/java/com/recordit/server/util/SessionUtilTest.java new file mode 100644 index 00000000..bfd3469f --- /dev/null +++ b/src/test/java/com/recordit/server/util/SessionUtilTest.java @@ -0,0 +1,80 @@ +package com.recordit.server.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpSession; + +import com.recordit.server.exception.member.NotFoundUserInfoInSessionException; + +@ExtendWith(MockitoExtension.class) +public class SessionUtilTest { + + @InjectMocks + private SessionUtil sessionUtil; + + @Spy + MockHttpSession mockHttpSession; + + @Test + @DisplayName("세션에 userId를 저장하는 기능을 테스트한다") + void 세션에_UserId를_저장하는_기능을_테스트한다() { + // given + + // when + sessionUtil.saveUserIdInSession(1L); + + // then + assertThat(mockHttpSession.getAttribute("LOGIN_USER_ID")).isEqualTo((Long)1L); + } + + @Nested + @DisplayName("세션에서 userId를 찾을 때") + class 세션에서_userId를_찾을_때 { + + @Test + @DisplayName("세션에 userId가 있으면 값이 찾아와진다") + void 세션에_정상적으로_userId를_저장하고_있으면_정상적으로_값이_찾아와진다() { + // given + Integer userId = 1; // Session Serializer 때문에 원래 Long을 넣어야 하지만 테스트에서 Integer를 사용 + mockHttpSession.setAttribute("LOGIN_USER_ID", userId); + + // when + Long userIdBySession = sessionUtil.findUserIdBySession(); + + // then + assertThat(userIdBySession.longValue()).isEqualTo(1L); + assertThatCode(() -> sessionUtil.findUserIdBySession()).doesNotThrowAnyException(); + } + + @Test + @DisplayName("세션에 userId가 없으면 예외를 던진다") + void 세션에_userId가_없으면_예외를_던진다() { + // given + + // when, then + assertThatThrownBy(() -> sessionUtil.findUserIdBySession()) + .isInstanceOf(NotFoundUserInfoInSessionException.class); + + } + } + + @Test + @DisplayName("세션을 삭제하는 기능을 테스트한다") + void 세션을_삭제하는_기능을_테스트한다() { + // given + + // when + sessionUtil.invalidateSession(); + + // then + assertThat(mockHttpSession.isInvalid()).isTrue(); + } + +} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 00000000..2a0bc97e --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,52 @@ +spring: + config: + activate: + on-profile: "test" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:~/RecordIt + username: sa + password: + h2: + console: + enabled: true + + redis: + host: localhost + port: 63790 + password: + session: + store-type: redis +springfox: + documentation: + swagger: + use-model-v3: false + +oauth: + kakao: + client-id: ${OAUTH_KAKAO_CLIENT_ID:} + client-secret: ${OAUTH_KAKAO_CLIENT_SECRET:} + redirect-url: ${OAUTH_KAKAO_REDIRECT_URL:} + token-request-url: https://kauth.kakao.com/oauth/token + user-info-request-url: https://kapi.kakao.com/v2/user/me + google: + client-id: ${OAUTH_GOOGLE_CLIENT_ID:} + client-secret: ${OAUTH_GOOGLE_CLIENT_SECRET:} + redirect-url: ${OAUTH_GOOGLE_REDIRECT_URL:} + token-request-url: https://oauth2.googleapis.com/token + user-info-request-url: https://oauth2.googleapis.com/tokeninfo + +s3: + credentials: + access-key: ${S3_ACCESS_KEY:} + secret-key: ${S3_SECRET_ACCESS_KEY:} + bucket: ${S3_BUCKET_NAME:} + directory: ${S3_DIRECTORY_NAME:} + region: ap-northeast-2 + +cors: + origin: ${CORS_ORIGIN_NAME:}