├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── application-dev1.yml │ │ ├── application-dev2.yml │ │ ├── application.yml │ │ ├── logback-spring.xml │ │ ├── application-dev.yml │ │ ├── application-local.yml │ │ └── logback.xml │ └── java │ │ └── com │ │ └── may │ │ └── ars │ │ ├── utils │ │ ├── auth │ │ │ ├── MemberContext.java │ │ │ ├── AuthCheck.java │ │ │ └── AuthCheckAspect.java │ │ └── scheduler │ │ │ └── SlackScheduler.java │ │ ├── domain │ │ ├── problem │ │ │ ├── ProblemTagRepository.java │ │ │ ├── TagRepository.java │ │ │ ├── Tag.java │ │ │ ├── ProblemTag.java │ │ │ ├── TagQueryRepository.java │ │ │ ├── ProblemRepository.java │ │ │ ├── Problem.java │ │ │ └── ProblemQueryRepository.java │ │ ├── guest │ │ │ ├── GuestBookRepository.java │ │ │ └── GuestBook.java │ │ ├── member │ │ │ ├── MemberRepository.java │ │ │ └── Member.java │ │ ├── review │ │ │ ├── ReviewRepository.java │ │ │ ├── Review.java │ │ │ └── ReviewQueryRepository.java │ │ └── BaseEntity.java │ │ ├── dto │ │ ├── member │ │ │ ├── LoginSuccessDto.java │ │ │ ├── GoogleTokenDto.java │ │ │ ├── JwtPayload.java │ │ │ └── KakaoProfile.java │ │ ├── problem │ │ │ ├── request │ │ │ │ ├── ProblemStepUpdateDto.java │ │ │ │ ├── ProblemNotificationUpdateDto.java │ │ │ │ ├── ProblemUpdateDto.java │ │ │ │ └── ProblemRequestDto.java │ │ │ └── response │ │ │ │ ├── TagDto.java │ │ │ │ ├── ProblemOnlyDto.java │ │ │ │ └── ProblemDto.java │ │ ├── guest │ │ │ ├── GuestRequestDto.java │ │ │ └── GuestResponseDto.java │ │ ├── review │ │ │ ├── SearchDto.java │ │ │ └── ReviewRequestDto.java │ │ ├── slack │ │ │ ├── Attachment.java │ │ │ └── Message.java │ │ └── common │ │ │ └── ResponseDto.java │ │ ├── common │ │ ├── advice │ │ │ ├── exception │ │ │ │ ├── JwtException.java │ │ │ │ ├── EntityNotFoundException.java │ │ │ │ ├── JsonWriteException.java │ │ │ │ ├── UserAuthenticationException.java │ │ │ │ └── BusinessException.java │ │ │ ├── ExceptionCode.java │ │ │ └── GlobalExceptionHandler.java │ │ └── message │ │ │ └── SuccessMessage.java │ │ ├── enums │ │ ├── RoleType.java │ │ └── SocialType.java │ │ ├── mapper │ │ ├── Default.java │ │ ├── GuestMapper.java │ │ ├── ReviewMapper.java │ │ └── ProblemMapper.java │ │ ├── config │ │ ├── SchedulerConfig.java │ │ ├── QuerydslConfig.java │ │ ├── properties │ │ │ └── GoogleProperties.java │ │ ├── JasyptConfig.java │ │ ├── WebConfig.java │ │ ├── RestTemplateConfig.java │ │ └── RedisCacheConfig.java │ │ ├── service │ │ ├── SearchService.java │ │ ├── MemberService.java │ │ ├── TagService.java │ │ ├── GuestBookService.java │ │ ├── ReviewService.java │ │ ├── JwtService.java │ │ ├── OauthService.java │ │ ├── SlackBotService.java │ │ └── ProblemService.java │ │ ├── controller │ │ ├── ProfileApiController.java │ │ ├── TagApiController.java │ │ ├── MemberApiController.java │ │ ├── SearchApiController.java │ │ ├── ReviewApiController.java │ │ ├── GuestApiController.java │ │ └── ProblemApiController.java │ │ └── ArsApplication.java └── test │ ├── java │ └── com │ │ └── may │ │ └── ars │ │ ├── ArsApplicationTests.java │ │ ├── domain │ │ ├── review │ │ │ ├── ReviewQueryRepositoryTest.java │ │ │ └── ReviewRepositoryTest.java │ │ ├── member │ │ │ └── MemberRepositoryTest.java │ │ └── problem │ │ │ ├── ProblemQueryRepositoryTest.java │ │ │ └── ProblemRepositoryTest.java │ │ ├── service │ │ ├── SlackBotServiceTest.java │ │ ├── ReviewServiceTest.java │ │ └── ProblemServiceTest.java │ │ ├── controller │ │ ├── ProblemApiControllerTest.java │ │ └── SearchApiControllerTest.java │ │ ├── mapper │ │ ├── GuestMapperTest.java │ │ ├── ProblemMapperTest.java │ │ └── ReviewMapperTest.java │ │ └── dto │ │ ├── guest │ │ └── GuestRequestDtoTest.java │ │ └── problem │ │ └── request │ │ └── ProblemRequestDtoTest.java │ └── resources │ └── application.yml ├── .gitignore ├── appspec.yml ├── scripts ├── switch.sh ├── stop.sh ├── profile.sh ├── deploy.sh ├── start.sh └── health.sh ├── README.md ├── .github └── workflows │ └── gradle.yml ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'ars' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayoung0073/ARS-backend/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/application-dev1.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | config: 3 | use-legacy-processing: true 4 | profiles: 5 | include: dev 6 | server: 7 | port: 8082 8 | servlet: 9 | context-path: / -------------------------------------------------------------------------------- /src/main/resources/application-dev2.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | config: 3 | use-legacy-processing: true 4 | profiles: 5 | include: dev 6 | server: 7 | port: 8083 8 | servlet: 9 | context-path: / -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/utils/auth/MemberContext.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.utils.auth; 2 | 3 | import com.may.ars.domain.member.Member; 4 | 5 | public class MemberContext { 6 | public static ThreadLocal currentMember = new ThreadLocal<>(); 7 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/ProblemTagRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface ProblemTagRepository extends JpaRepository { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/member/LoginSuccessDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.member; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | @Builder 7 | @Getter 8 | public class LoginSuccessDto { 9 | private String nickname; 10 | private String access_token; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/member/GoogleTokenDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.member; 2 | 3 | import lombok.Getter; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | 7 | @Getter 8 | public class GoogleTokenDto { 9 | 10 | @NotBlank 11 | private String accessToken; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/exception/JwtException.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice.exception; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | 5 | public class JwtException extends BusinessException { 6 | public JwtException() { 7 | super(ExceptionCode.JWT_EXCEPTION); 8 | } 9 | } -------------------------------------------------------------------------------- /src/test/java/com/may/ars/ArsApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.may.ars; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ArsApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/request/ProblemStepUpdateDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.request; 2 | 3 | import lombok.Getter; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | @Getter 8 | public class ProblemStepUpdateDto { 9 | 10 | @NotNull 11 | private int step; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/enums/RoleType.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.enums; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum RoleType { 7 | ADMIN("ROLE_ADMIN"), USER("ROLE_USER"); 8 | 9 | private String roleName; 10 | 11 | RoleType(String roleName) { 12 | this.roleName = roleName; 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/TagRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface TagRepository extends JpaRepository { 8 | 9 | Optional findByTagName(String tagName); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/exception/EntityNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice.exception; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | 5 | public class EntityNotFoundException extends BusinessException { 6 | public EntityNotFoundException() { 7 | super(ExceptionCode.ENTITY_NOT_FOUND); 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/exception/JsonWriteException.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice.exception; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | 5 | public class JsonWriteException extends BusinessException { 6 | public JsonWriteException() { 7 | super(ExceptionCode.JSON_WRITE_ERROR); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/enums/SocialType.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.enums; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum SocialType { 7 | KAKAO("SOCIAL_KAKAO"), GOOGLE("SOCIAL_GOOGLE"); 8 | 9 | private String socialName; 10 | 11 | SocialType(String socialName) { 12 | this.socialName = socialName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/exception/UserAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice.exception; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | 5 | public class UserAuthenticationException extends BusinessException { 6 | public UserAuthenticationException() { 7 | super(ExceptionCode.NOT_VALID_USER); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/mapper/Default.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.CONSTRUCTOR) 9 | @Retention(RetentionPolicy.CLASS) 10 | public @interface Default { 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/request/ProblemNotificationUpdateDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.request; 2 | 3 | import lombok.Getter; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.time.LocalDate; 7 | 8 | @Getter 9 | public class ProblemNotificationUpdateDto { 10 | 11 | @NotNull 12 | private LocalDate notificationDate; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/utils/auth/AuthCheck.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.utils.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.METHOD}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface AuthCheck { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/guest/GuestRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.guest; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | @Builder 9 | @Getter 10 | public class GuestRequestDto { 11 | 12 | @NotBlank 13 | private String nickname; 14 | 15 | @NotBlank 16 | private String content; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/response/TagDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.response; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor 9 | @AllArgsConstructor 10 | @Getter 11 | @Builder 12 | public class TagDto { 13 | private String tagName; 14 | private Long count; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/guest/GuestResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.guest; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.time.LocalDate; 7 | 8 | @Getter @Setter 9 | public class GuestResponseDto { 10 | 11 | private Long id; 12 | 13 | private String nickname; 14 | 15 | private String content; 16 | 17 | private LocalDate createdDate; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/guest/GuestBookRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.guest; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface GuestBookRepository extends JpaRepository { 9 | List findAllByOrderByCreatedDateDesc(Pageable pageable); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/member/JwtPayload.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.member; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | @Getter 7 | @NoArgsConstructor 8 | public class JwtPayload { 9 | 10 | private Long id; 11 | private String email; 12 | 13 | public JwtPayload(Long id, String email) { 14 | this.id = id; 15 | this.email = email; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | jasypt: 2 | encryptor: 3 | bean: encryptorBean 4 | 5 | --- 6 | 7 | spring: 8 | profiles: 9 | active: dev1 10 | 11 | server: 12 | port: 8082 13 | 14 | --- 15 | 16 | spring: 17 | profiles: 18 | active: dev2 19 | 20 | server: 21 | port: 8083 22 | 23 | --- 24 | 25 | spring: 26 | profiles: 27 | active: local 28 | 29 | server: 30 | port: 8080 31 | 32 | --- 33 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/member/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.member; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public interface MemberRepository extends JpaRepository { 9 | 10 | Optional findByEmail(String email); 11 | 12 | List findAllBySlackIdNull(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | -------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | 4 | files: 5 | - source: / 6 | destination: /home/ec2-user/jenkins 7 | overwrite: yes 8 | 9 | hooks: 10 | AfterInstall: 11 | - location: stop.sh # 엔진엑스와 연결되어 있지 않은 스프링부트를 종료한다. 12 | timeout: 60 13 | ApplicationStart: 14 | - location: start.sh # 엔진엑스와 연결되어 있지 않은 포트로 새 버전의 스프링부트를 시작한다. 15 | timeout: 60 16 | ValidateService: 17 | - location: health.sh # 새 스프링부트가 정상적으로 실행됐는지 확인한다. 18 | timeout: 60 19 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/review/SearchDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.review; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.time.LocalDate; 7 | 8 | @Getter @Setter 9 | public class SearchDto { 10 | 11 | private Long id; 12 | 13 | private Long problemId; 14 | 15 | private String title; 16 | 17 | private int step; 18 | 19 | private String link; 20 | 21 | private LocalDate createdDate; 22 | 23 | private String content; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | jasypt: 2 | encryptor: 3 | bean: encryptorBean 4 | password: tempPassword 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE 9 | jpa: 10 | hibernate: 11 | ddl-auto: update 12 | show-sql: false 13 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 14 | 15 | ars: 16 | secret_key: testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest 17 | 18 | slack: 19 | token: temptoken 20 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice.exception; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | public class BusinessException extends RuntimeException { 8 | 9 | private final ExceptionCode exceptionCode; 10 | 11 | public BusinessException(ExceptionCode exceptionCode) { 12 | super(exceptionCode.getMessage()); 13 | this.exceptionCode = exceptionCode; 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/review/ReviewRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.review; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | import java.time.LocalDate; 9 | import java.util.ArrayList; 10 | 11 | @Builder 12 | @Getter 13 | public class ReviewRequestDto { 14 | 15 | @NotBlank 16 | private String content; 17 | 18 | @NotNull 19 | private LocalDate notificationDate; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/review/ReviewRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.review; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.util.Optional; 7 | 8 | public interface ReviewRepository extends JpaRepository { 9 | 10 | @Query(value = "SELECT * FROM review JOIN problem p on review_id = :reviewId AND p.member_id = :memberId", nativeQuery = true) 11 | Optional findReviewByIdAndMemberId(Long reviewId, Long memberId); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/mapper/GuestMapper.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.guest.GuestBook; 4 | import com.may.ars.dto.guest.GuestRequestDto; 5 | import com.may.ars.dto.guest.GuestResponseDto; 6 | import org.mapstruct.Mapper; 7 | import org.mapstruct.Mapping; 8 | import org.mapstruct.factory.Mappers; 9 | 10 | @Mapper(componentModel = "spring") 11 | public interface GuestMapper { 12 | 13 | @Mapping(target = "id", ignore = true) 14 | GuestBook toEntity(GuestRequestDto requestDto); 15 | 16 | GuestResponseDto toDto(GuestBook guestBook); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/SchedulerConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import net.javacrumbs.shedlock.core.LockProvider; 4 | import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import javax.sql.DataSource; 9 | 10 | @Configuration 11 | public class SchedulerConfig { 12 | 13 | @Bean 14 | public LockProvider lockProvider(DataSource dataSource) { 15 | return new JdbcTemplateLockProvider(dataSource); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/Tag.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 8 | @AllArgsConstructor 9 | @Getter 10 | @Entity 11 | @Builder 12 | public class Tag { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Column(name = "tag_id") 17 | private Long id; 18 | 19 | @Column(unique = true) 20 | private String tagName; 21 | 22 | public Tag(String tagName) { 23 | this.tagName = tagName; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /scripts/switch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ABSPATH=$(readlink -f $0) 4 | ABSDIR=$(dirname $ABSPATH) 5 | source ${ABSDIR}/profile.sh 6 | 7 | function switch_proxy() { 8 | IDLE_PORT=$(find_idle_port) 9 | 10 | echo "> 전환할 Port: $IDLE_PORT" >> /home/ec2-user/log/deploy.log 11 | echo "> Port 전환" >> /home/ec2-user/log/deploy.log 12 | echo "set \$service_url http://172.31.33.165:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc 13 | 14 | echo "> 엔진엑스 Reload : docker exec -d nginx nginx -s reload" >> /home/ec2-user/log/deploy.log 15 | sudo docker exec -d nginx nginx -s reload 16 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/QuerydslConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import javax.persistence.EntityManager; 8 | import javax.persistence.PersistenceContext; 9 | 10 | @Configuration 11 | public class QuerydslConfig { 12 | 13 | @PersistenceContext 14 | private EntityManager entityManager; 15 | 16 | @Bean 17 | public JPAQueryFactory jpaQueryFactory() { 18 | return new JPAQueryFactory(entityManager); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/guest/GuestBook.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.guest; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | 7 | import com.may.ars.domain.BaseEntity; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | @Getter 12 | @Builder 13 | @Entity 14 | public class GuestBook extends BaseEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "guest_id") 19 | private Long id; 20 | 21 | @Column 22 | private String nickname; 23 | 24 | @Column 25 | private String content; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/properties/GoogleProperties.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config.properties; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConfigurationProperties(prefix = "social.google") 10 | @Getter 11 | @Setter 12 | public class GoogleProperties { 13 | private String clientId; 14 | private String secretKey; 15 | private String redirectUri; 16 | private String tokenRequestUrl; 17 | private String profileRequestUrl; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/SearchService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.review.Review; 4 | import com.may.ars.domain.review.ReviewQueryRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @RequiredArgsConstructor 11 | @Service 12 | public class SearchService { 13 | 14 | private final ReviewQueryRepository reviewQueryRepository; 15 | 16 | public List search(String keyword, Long reviewId, int size) { 17 | return reviewQueryRepository.search(keyword, reviewId, size); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/request/ProblemUpdateDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.request; 2 | 3 | 4 | import lombok.*; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | import javax.validation.constraints.NotNull; 8 | import java.time.LocalDate; 9 | import java.util.ArrayList; 10 | 11 | 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @Getter @Setter 15 | @Builder 16 | public class ProblemUpdateDto { 17 | 18 | @NotBlank 19 | private String title; 20 | 21 | @NotNull 22 | private String link; 23 | 24 | @NotNull 25 | private ArrayList tagList; 26 | 27 | @NotNull 28 | private LocalDate notificationDate; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /scripts/stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ABSPATH=$(readlink -f $0) 4 | ABSDIR=$(dirname $ABSPATH) 5 | source ${ABSDIR}/profile.sh 6 | 7 | IDLE_PROFILE=$(find_idle_profile) 8 | echo "> 현재 프로필 : ${IDLE_PROFILE}" >> /home/ec2-user/log/deploy.log 9 | 10 | CONTAINER_ID=$(docker container ls -f name=${IDLE_PROFILE} -q) 11 | echo "> 컨테이너 ID : ${CONTAINER_ID}" >> /home/ec2-user/log/deploy.log 12 | 13 | if [ -z ${CONTAINER_ID} ] 14 | then 15 | echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/log/deploy.log 16 | else 17 | echo "> docker stop $IDLE_PROFILE && sudo docker rm $IDLE_PROFILE" >> /home/ec2-user/log/deploy.log 18 | docker stop $IDLE_PROFILE && sudo docker rm $IDLE_PROFILE 19 | sleep 5 20 | fi -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/slack/Attachment.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.slack; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Builder; 8 | import lombok.NoArgsConstructor; 9 | import lombok.ToString; 10 | 11 | @JsonInclude(JsonInclude.Include.NON_NULL) 12 | @JsonPropertyOrder({ 13 | "pretext", 14 | "text" 15 | }) 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Builder 19 | @ToString 20 | public class Attachment { 21 | 22 | @JsonProperty("pretext") 23 | public final String pretext = ""; 24 | 25 | @JsonProperty("text") 26 | public String text; 27 | 28 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/ProfileApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.core.env.Environment; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.Arrays; 10 | 11 | @AllArgsConstructor 12 | @RequestMapping("/api") 13 | @RestController 14 | public class ProfileApiController { 15 | 16 | private final Environment env; 17 | 18 | @GetMapping("/profile") 19 | public String getProfiles() { 20 | return Arrays.stream(env.getActiveProfiles()) 21 | .findFirst() 22 | .orElse(""); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain; 2 | 3 | import lombok.Getter; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedDate; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.EntityListeners; 10 | import javax.persistence.MappedSuperclass; 11 | import java.time.LocalDateTime; 12 | 13 | @Getter 14 | @MappedSuperclass 15 | @EntityListeners(AuditingEntityListener.class) 16 | public abstract class BaseEntity { 17 | 18 | @CreatedDate 19 | @Column(updatable = false) 20 | private LocalDateTime createdDate; 21 | 22 | @LastModifiedDate 23 | private LocalDateTime modifiedDate; 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/response/ProblemOnlyDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.may.ars.domain.problem.ProblemTag; 5 | import lombok.*; 6 | 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter @Setter 14 | @Builder 15 | public class ProblemOnlyDto { 16 | 17 | private Long id; 18 | 19 | private String title; 20 | 21 | private String link; 22 | 23 | private int step; 24 | 25 | private LocalDate notificationDate; 26 | 27 | private LocalDateTime modifiedDate; 28 | 29 | @JsonIgnoreProperties({"problem"}) 30 | private List tagList; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/ArsApplication.java: -------------------------------------------------------------------------------- 1 | package com.may.ars; 2 | 3 | import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.cache.annotation.EnableCaching; 7 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @EnableCaching 11 | @EnableJpaAuditing 12 | @EnableScheduling 13 | @EnableSchedulerLock(defaultLockAtMostFor = "PT5M") 14 | @SpringBootApplication 15 | public class ArsApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(ArsApplication.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/review/Review.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.review; 2 | 3 | import com.may.ars.domain.BaseEntity; 4 | import com.may.ars.domain.problem.Problem; 5 | import lombok.*; 6 | import org.hibernate.annotations.Type; 7 | 8 | import javax.persistence.*; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | @Getter 13 | @Entity 14 | @Builder 15 | public class Review extends BaseEntity { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "review_id", updatable = false) 20 | private Long id; 21 | 22 | @ManyToOne 23 | @JoinColumn(name = "problem_id", updatable = false) 24 | private Problem problem; 25 | 26 | @Lob 27 | @Type(type = "text") 28 | private String content; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/request/ProblemRequestDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.request; 2 | 3 | import lombok.*; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.NotNull; 7 | import java.time.LocalDate; 8 | import java.util.ArrayList; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | @Getter @Setter 13 | @Builder 14 | public class ProblemRequestDto { 15 | 16 | @NotBlank 17 | private String title; 18 | 19 | @NotNull 20 | private String link; 21 | 22 | @Builder.Default 23 | private int step = 1; 24 | 25 | @NotNull 26 | private LocalDate notificationDate; 27 | 28 | @NotNull 29 | private ArrayList tagList; 30 | 31 | @NotBlank 32 | private String content; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /scripts/profile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function find_idle_profile() 4 | { 5 | RESPONSE_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://172.31.33.165/api/profile) 6 | 7 | if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함) 8 | then 9 | CURRENT_PROFILE=dev2 10 | else 11 | CURRENT_PROFILE=$(curl -s http://172.31.33.165/api/profile) 12 | fi 13 | 14 | if [ ${CURRENT_PROFILE} == dev1 ] 15 | then 16 | IDLE_PROFILE=dev2 17 | else 18 | IDLE_PROFILE=dev1 19 | fi 20 | 21 | echo "${IDLE_PROFILE}" 22 | } 23 | 24 | # 쉬고 있는 profile 의 port 찾기 25 | function find_idle_port() 26 | { 27 | IDLE_PROFILE=$(find_idle_profile) 28 | 29 | if [ ${IDLE_PROFILE} == dev1 ] 30 | then 31 | echo "8082" 32 | else 33 | echo "8083" 34 | fi 35 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/problem/response/ProblemDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.may.ars.domain.problem.ProblemTag; 5 | import com.may.ars.domain.review.Review; 6 | import lombok.*; 7 | import java.time.LocalDate; 8 | import java.util.List; 9 | 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | @Getter @Setter 13 | @Builder 14 | public class ProblemDto { 15 | 16 | private Long id; 17 | 18 | private String title; 19 | 20 | private String link; 21 | 22 | private int step; 23 | 24 | private LocalDate notificationDate; 25 | 26 | @JsonIgnoreProperties({"problem"}) 27 | private List reviewList; 28 | 29 | @JsonIgnoreProperties({"problem"}) 30 | private List tagList; 31 | } 32 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BUILD_JAR=$(ls /home/ec2-user/jenkins/build/libs/*.jar) 3 | JAR_NAME=$(basename $BUILD_JAR) 4 | echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/deploy.log 5 | 6 | echo "> build 파일 복사" >> /home/ec2-user/deploy.log 7 | DEPLOY_PATH=/home/ec2-user/ 8 | cp $BUILD_JAR $DEPLOY_PATH 9 | 10 | echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/deploy.log 11 | CURRENT_PID=$(pgrep -f $JAR_NAME) 12 | 13 | if [ -z $CURRENT_PID ] 14 | then 15 | echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log 16 | else 17 | echo "> kill -15 $CURRENT_PID" 18 | kill -15 $CURRENT_PID 19 | sleep 5 20 | fi 21 | 22 | DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME 23 | echo "> DEPLOY_JAR 배포" >> /home/ec2-user/deploy.log 24 | nohup java -jar -Dspring.profiles.active=dev $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/deploy_err.log & -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/ProblemTag.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | @Getter 13 | @Entity 14 | public class ProblemTag { 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | private Long id; 18 | 19 | @ManyToOne 20 | @JoinColumn(name = "problem_id") 21 | private Problem problem; 22 | 23 | @ManyToOne(cascade = CascadeType.ALL) 24 | @JoinColumn(name = "tag_id") 25 | private Tag tag; 26 | 27 | public ProblemTag(Problem problem, Tag tag) { 28 | this.problem = problem; 29 | this.tag = tag; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/domain/review/ReviewQueryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.review; 2 | 3 | import com.may.ars.domain.problem.Problem; 4 | import com.may.ars.domain.problem.ProblemRepository; 5 | import com.may.ars.dto.problem.response.ProblemOnlyDto; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.List; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.is; 15 | 16 | @SpringBootTest 17 | class ReviewQueryRepositoryTest { 18 | 19 | @Autowired 20 | private ReviewQueryRepository reviewQueryRepository; 21 | 22 | @Autowired 23 | private ProblemRepository problemRepository; 24 | 25 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/member/KakaoProfile.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.member; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class KakaoProfile { 7 | 8 | public Integer id; 9 | public String connected_at; 10 | public Properties properties; 11 | public KakaoAccount kakao_account; 12 | 13 | @Data 14 | public static class Properties { 15 | public String nickname; 16 | } 17 | 18 | @Data 19 | public static class KakaoAccount { 20 | public Boolean profile_needs_agreement; 21 | public Profile profile; 22 | public Boolean has_email; 23 | public Boolean email_needs_agreement; 24 | public Boolean is_email_valid; 25 | public Boolean is_email_verified; 26 | public String email; 27 | } 28 | 29 | @Data 30 | public static class Profile { 31 | public String nickname; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/slack/Message.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.slack; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.NoArgsConstructor; 11 | import lombok.ToString; 12 | 13 | @JsonInclude(JsonInclude.Include.NON_NULL) 14 | @JsonPropertyOrder({ 15 | "channel", 16 | "blocks" 17 | }) 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | @Builder 21 | @ToString 22 | public class Message { 23 | 24 | @JsonProperty("channel") 25 | private String channel; 26 | 27 | @JsonProperty("tex") 28 | private String text; 29 | 30 | @JsonProperty("attachments") 31 | @Builder.Default 32 | public List attachments = new ArrayList<>(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/JasyptConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 4 | import org.jasypt.encryption.StringEncryptor; 5 | import org.jasypt.encryption.pbe.PooledPBEStringEncryptor; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class JasyptConfig { 12 | 13 | @Value("${jasypt.encryptor.password}") 14 | private String encryptKey; 15 | 16 | @Bean("encryptorBean") 17 | public StringEncryptor stringEncryptor() { 18 | PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor(); 19 | encryptor.setProvider(new BouncyCastleProvider()); 20 | encryptor.setPoolSize(1); 21 | encryptor.setPassword(encryptKey); 22 | encryptor.setAlgorithm("PBEWithSHA256And128BitAES-CBC-BC"); 23 | return encryptor; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/MemberService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.member.MemberRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class MemberService { 15 | 16 | private final MemberRepository memberRepository; 17 | 18 | @Transactional(readOnly = true) 19 | public Optional findMemberByEmail(String email) { 20 | return memberRepository.findByEmail(email); 21 | } 22 | 23 | @Transactional 24 | public List findAllBySlackIdNull() { 25 | return memberRepository.findAllBySlackIdNull(); 26 | } 27 | 28 | @Transactional 29 | public void saveMember(Member member) { 30 | memberRepository.save(member); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/TagQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import com.querydsl.core.Tuple; 4 | import com.querydsl.jpa.impl.JPAQueryFactory; 5 | import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import static com.may.ars.domain.problem.QProblemTag.problemTag; 9 | 10 | import java.util.List; 11 | 12 | @Repository 13 | public class TagQueryRepository extends QuerydslRepositorySupport { 14 | 15 | private final JPAQueryFactory queryFactory; 16 | 17 | public TagQueryRepository(JPAQueryFactory queryFactory) { 18 | super(ProblemTag.class); 19 | this.queryFactory = queryFactory; 20 | } 21 | 22 | public List findAllTagList() { 23 | return queryFactory 24 | .from(problemTag) 25 | .groupBy(problemTag.tag) 26 | .select(problemTag.tag.tagName, problemTag.tag.count()) 27 | .fetch(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/TagApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.dto.common.ResponseDto; 4 | import com.may.ars.service.TagService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import static com.may.ars.common.message.SuccessMessage.SUCCESS_GET_TAG_LIST; 13 | 14 | @RequiredArgsConstructor 15 | @RestController 16 | @RequestMapping("/api") 17 | public class TagApiController { 18 | 19 | private final TagService tagService; 20 | 21 | @GetMapping("/tags") 22 | public ResponseEntity getProblemListByTag() { 23 | return ResponseEntity.ok().body(ResponseDto.of( 24 | HttpStatus.OK, SUCCESS_GET_TAG_LIST, tagService.getAllTagList()) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import org.springframework.boot.web.servlet.view.MustacheViewResolver; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.HttpMethod; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | public class WebConfig implements WebMvcConfigurer { 12 | @Override 13 | public void addCorsMappings(CorsRegistry registry) { 14 | registry.addMapping("/**") 15 | .allowedOrigins("*") 16 | .allowedMethods( 17 | HttpMethod.GET.name(), 18 | HttpMethod.POST.name(), 19 | HttpMethod.PUT.name(), 20 | HttpMethod.DELETE.name()) 21 | .allowCredentials(false); 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/ExceptionCode.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @AllArgsConstructor 8 | @Getter 9 | public enum ExceptionCode { 10 | 11 | // Common 12 | ENTITY_NOT_FOUND(HttpStatus.BAD_REQUEST, "Entity Not Found"), 13 | INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "Invalid Type Value"), 14 | INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "Invalid Input Value"), 15 | METHOD_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "Invalid Method"), 16 | JSON_WRITE_ERROR(HttpStatus.BAD_REQUEST, "Json Write Error"), 17 | INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error"), 18 | SERVLET_ERROR(HttpStatus.BAD_REQUEST, "Servlet Error"), 19 | 20 | // JWT 21 | JWT_EXCEPTION(HttpStatus.BAD_REQUEST, "Jwt Exception"), 22 | NOT_VALID_USER(HttpStatus.BAD_REQUEST, "Authorization Exception"); 23 | 24 | private final HttpStatus status; 25 | private final String message; 26 | 27 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/TagService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.problem.TagQueryRepository; 4 | import com.may.ars.dto.problem.response.TagDto; 5 | import com.querydsl.core.Tuple; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.cache.annotation.Cacheable; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @Slf4j 15 | @RequiredArgsConstructor 16 | @Service 17 | public class TagService { 18 | 19 | private final TagQueryRepository tagQueryRepository; 20 | 21 | @Cacheable(value = "tagCache") 22 | public List getAllTagList() { 23 | List tagList = new ArrayList<>(); 24 | List list = tagQueryRepository.findAllTagList(); 25 | log.info("Not Cached"); 26 | list.forEach(tuple -> tagList.add(TagDto.builder().tagName(tuple.get(0, String.class)).count(tuple.get(1, Long.class)).build())); 27 | return tagList; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/member/Member.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.member; 2 | 3 | import com.may.ars.enums.RoleType; 4 | import com.may.ars.enums.SocialType; 5 | import com.may.ars.domain.BaseEntity; 6 | import lombok.*; 7 | 8 | import javax.persistence.*; 9 | 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | @Getter 13 | @Builder 14 | @Entity 15 | public class Member extends BaseEntity { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "member_id") 20 | private Long id; 21 | 22 | @Column(unique = true) 23 | private String email; 24 | 25 | @Enumerated(EnumType.STRING) 26 | private RoleType roleType; 27 | 28 | @Enumerated(EnumType.STRING) 29 | private SocialType socialType; 30 | 31 | @Column(unique = true) 32 | private String socialId; 33 | 34 | @Column 35 | private String nickname; 36 | 37 | @Setter 38 | @Column(unique = true) 39 | private String slackId; 40 | 41 | @Setter 42 | private boolean checkSlack; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/utils/scheduler/SlackScheduler.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.utils.scheduler; 2 | 3 | import com.may.ars.domain.problem.Problem; 4 | import com.may.ars.domain.problem.ProblemRepository; 5 | import com.may.ars.service.SlackBotService; 6 | import lombok.RequiredArgsConstructor; 7 | import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.time.LocalDate; 12 | import java.util.List; 13 | 14 | @RequiredArgsConstructor 15 | @Component 16 | public class SlackScheduler { 17 | 18 | private final SlackBotService slackBotService; 19 | private final ProblemRepository problemRepository; 20 | 21 | @Scheduled(cron = "0 0 9 * * *") // 매일 오전 9시마다 22 | @SchedulerLock(name = "SlackScheduler_notification") 23 | public void notification() { 24 | List problemList = problemRepository.findAllByNotificationDate(LocalDate.now()); 25 | problemList.forEach(slackBotService::notification); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/service/SlackBotServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.member.MemberRepository; 5 | import com.may.ars.domain.problem.Problem; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | @SpringBootTest 11 | class SlackBotServiceTest { 12 | 13 | @Autowired 14 | private SlackBotService slackBotService; 15 | 16 | @Autowired 17 | private ProblemService problemService; 18 | 19 | @Autowired 20 | private MemberRepository memberRepository; 21 | 22 | // @Test 23 | void 유저_정보_By_이메일_테스트() { 24 | // given 25 | Member member = Member.builder() 26 | .email("ayong0310@naver.com") 27 | .slackId("test") 28 | .build(); 29 | memberRepository.save(member); 30 | 31 | // when 32 | slackBotService.getSlackIdByEmail(member.getEmail()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/domain/member/MemberRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.member; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 6 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 11 | @DataJpaTest 12 | public class MemberRepositoryTest { 13 | 14 | @Autowired 15 | private MemberRepository memberRepository; 16 | 17 | @Test 18 | void 멤버_저장_테스트() { 19 | // given 20 | Member member = Member.builder() 21 | .email("ayong0310@naver.com") 22 | .nickname("문아영") 23 | .build(); 24 | 25 | // when 26 | memberRepository.save(member); 27 | 28 | // then 29 | assertThat(memberRepository.findByEmail(member.getEmail()).isPresent()).isEqualTo(true); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/ProblemRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | public interface ProblemRepository extends JpaRepository { 13 | 14 | List findByOrderByModifiedDateDesc(Pageable pageable); // 최신 업데이트순 15 | 16 | // 업데이트 날짜가 같은 경우를 고려해 (포함되지 않아야 할) cursorId 추가 17 | List findByModifiedDateBeforeAndIdNotOrderByModifiedDateDesc(LocalDateTime modifiedDate, Long id, Pageable pageable); // 커서 기반 페이징 (업데이트순) 18 | 19 | Optional findProblemByIdAndWriter(Long id, Member writer); 20 | 21 | void deleteProblemById(Long id); 22 | 23 | List findAllByNotificationDate(LocalDate date); 24 | 25 | List findAllByStepOrderByModifiedDateDesc(int step, Pageable pageable); 26 | 27 | List findByIdLessThanAndStepOrderByIdDesc(Long id, int step, Pageable pageable); // 커서 기반 페이징 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/domain/review/ReviewRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.review; 2 | 3 | import com.may.ars.domain.problem.ProblemRepository; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.ActiveProfiles; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.is; 12 | 13 | @ActiveProfiles("test") 14 | @SpringBootTest 15 | @Transactional 16 | class ReviewRepositoryTest { 17 | 18 | @Autowired 19 | private ReviewRepository reviewRepository; 20 | 21 | @Test 22 | void 리뷰_수정_테스트() { 23 | // given 24 | String content = "TEST"; 25 | Review review = Review.builder() 26 | .content(content) 27 | .build(); 28 | 29 | // when 30 | reviewRepository.save(review); 31 | 32 | // then 33 | Review result = reviewRepository.findById(review.getId()).get(); 34 | assertThat(result.getContent(), is(content)); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.http.client.BufferingClientHttpRequestFactory; 7 | import org.springframework.http.client.SimpleClientHttpRequestFactory; 8 | import org.springframework.http.converter.StringHttpMessageConverter; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import java.nio.charset.StandardCharsets; 12 | import java.time.Duration; 13 | 14 | @Configuration 15 | public class RestTemplateConfig { 16 | 17 | @Bean 18 | public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { 19 | return restTemplateBuilder 20 | .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory())) 21 | .setConnectTimeout(Duration.ofMillis(5000)) 22 | .setReadTimeout(Duration.ofMillis(5000)) 23 | .additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8)) 24 | .build(); 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ABSPATH=$(readlink -f $0) 4 | ABSDIR=$(dirname $ABSPATH) 5 | source ${ABSDIR}/profile.sh 6 | 7 | IDLE_PORT=$(find_idle_port) 8 | DEPLOY_PATH=/home/ec2-user/deploy/ 9 | REPOSITORY=/home/ec2-user/deploy 10 | 11 | BUILD_JAR=$(ls /home/ec2-user/jenkins/*.jar) 12 | JAR_NAME=$(basename $BUILD_JAR) 13 | 14 | echo "> Build 파일 복사 : cp $BUILD_JAR $DEPLOY_PATH" >> /home/ec2-user/log/deploy.log 15 | cp $BUILD_JAR $DEPLOY_PATH # 새로운 jar 파일을 덮어쓴다. 16 | 17 | DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME 18 | echo "> DEPLOY_JAR 배포" >> /home/ec2-user/log/deploy.log 19 | JAR_NAME=$(ls -tr $DEPLOY_PATH*.jar | tail -n 1) 20 | 21 | IDLE_PROFILE=$(find_idle_profile) 22 | 23 | echo "> $DEPLOY_PATH 의 $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다." >> /home/ec2-user/log/deploy.log 24 | echo "> cd $DEPLOY_PATH ; docker build -t ars ./" >> /home/ec2-user/log/deploy.log 25 | 26 | cd $DEPLOY_PATH 27 | 28 | docker build -t ars ./ 29 | echo "> docker run -it --name $IDLE_PROFILE --network=deploy_backend -d -e active=$IDLE_PROFILE -p $IDLE_PORT:$IDLE_PORT ars" >> /home/ec2-user/log/deploy.log 30 | docker run -it --name $IDLE_PROFILE --network=deploy_backend -d -e active=$IDLE_PROFILE -p $IDLE_PORT:$IDLE_PORT ars 31 | echo "> Success Docker run" >> /home/ec2-user/log/deploy.log 32 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ${SLACK_WEBHOOK_URI} 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n 8 | 9 | ars-log 10 | :ghost: 11 | true 12 | 13 | 14 | 15 | 16 | 17 | %d %-5level %logger{35} - %msg%n 18 | 19 | 20 | 21 | 22 | 23 | 24 | INFO 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /scripts/health.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ABSPATH=$(readlink -f $0) 4 | ABSDIR=$(dirname $ABSPATH) 5 | source ${ABSDIR}/profile.sh 6 | source ${ABSDIR}/switch.sh 7 | 8 | IDLE_PORT=$(find_idle_port) 9 | 10 | echo "> Health Check Start!" >> /home/ec2-user/log/deploy.log 11 | echo "> IDLE_PORT: $IDLE_PORT" >> /home/ec2-user/log/deploy.log 12 | echo "> curl -s http://172.31.33.165:$IDLE_PORT/api/profile " >> /home/ec2-user/log/deploy.log 13 | sleep 10 14 | 15 | for RETRY_COUNT in {1..10} 16 | do 17 | RESPONSE=$(curl -s http://172.31.33.165:${IDLE_PORT}/api/profile) 18 | UP_COUNT=$(echo ${RESPONSE} | grep 'dev' | wc -l) 19 | 20 | if [ ${UP_COUNT} -ge 1 ] 21 | then # $up_count >= 1 ("real" 문자열이 있는지 검증) 22 | echo "> Health check 성공" >> /home/ec2-user/log/deploy.log 23 | switch_proxy 24 | break 25 | else 26 | echo "> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다." >> /home/ec2-user/log/deploy.log 27 | echo "> Health check: ${RESPONSE}" >> /home/ec2-user/log/deploy.log 28 | fi 29 | 30 | if [ ${RETRY_COUNT} -eq 10 ] 31 | then 32 | echo "> Health check 실패. " >> /home/ec2-user/log/deploy.log 33 | echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다." >> /home/ec2-user/log/deploy.log 34 | exit 1 35 | fi 36 | 37 | echo "> Health check 연결 실패. 재시도..." >> /home/ec2-user/log/deploy.log 38 | sleep 10 39 | done -------------------------------------------------------------------------------- /src/main/java/com/may/ars/dto/common/ResponseDto.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.common; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.http.HttpStatus; 7 | 8 | import java.util.Optional; 9 | 10 | @Getter 11 | @Setter 12 | @AllArgsConstructor 13 | public class ResponseDto { 14 | 15 | private int status; 16 | private String message; 17 | private T data; 18 | 19 | public ResponseDto(int status, String message) { 20 | this.status = status; 21 | this.message = message; 22 | } 23 | 24 | public static ResponseDto of(HttpStatus httpStatus, String message) { 25 | int status = Optional.ofNullable(httpStatus) 26 | .orElse(HttpStatus.OK) 27 | .value(); 28 | return new ResponseDto<>(status, message); 29 | } 30 | 31 | public static ResponseDto of(HttpStatus httpStatus, String message, T data) { 32 | int status = Optional.ofNullable(httpStatus) 33 | .orElse(HttpStatus.OK) 34 | .value(); 35 | return new ResponseDto<>(status, message, data); 36 | } 37 | 38 | public static ResponseDto fail(HttpStatus httpStatus, String message) { 39 | return new ResponseDto<>(httpStatus.value(), message, null); 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/may/ars/controller/ProblemApiControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | import static org.hamcrest.Matchers.hasSize; 14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @SpringBootTest 18 | @AutoConfigureMockMvc 19 | class ProblemApiControllerTest { 20 | 21 | @Autowired 22 | protected MockMvc mockMvc; 23 | 24 | // @Test 25 | void 문제_페이징_테스트_default() throws Exception { 26 | mockMvc.perform( 27 | get("/api/problems") 28 | .contentType(MediaType.APPLICATION_JSON) 29 | ).andExpect(status().isOk()) 30 | .andExpect(MockMvcResultMatchers.jsonPath("$.data", hasSize(9))); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/mapper/ReviewMapper.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.problem.Problem; 4 | import com.may.ars.domain.review.Review; 5 | import com.may.ars.dto.problem.request.ProblemRequestDto; 6 | import com.may.ars.dto.review.SearchDto; 7 | import com.may.ars.dto.review.ReviewRequestDto; 8 | import org.mapstruct.Mapper; 9 | import org.mapstruct.Mapping; 10 | 11 | @Mapper(componentModel = "spring") 12 | public interface ReviewMapper { 13 | 14 | @Mapping(target = "problem", source = "problem") 15 | @Mapping(target = "id", ignore = true) 16 | Review toEntity(Problem problem, ProblemRequestDto requestDto); 17 | 18 | @Mapping(target = "problem", source = "problem") 19 | @Mapping(target = "id", ignore = true) 20 | Review toEntity(Problem problem, ReviewRequestDto requestDto); 21 | 22 | @Mapping(target = "problem", ignore = true) 23 | @Mapping(target = "id", source = "id") 24 | Review toEntity(Long id, ReviewRequestDto requestDto); 25 | 26 | @Mapping(target = "id", source="id") 27 | @Mapping(target = "problemId", source="problem.id") 28 | @Mapping(target = "title", source="problem.title") 29 | @Mapping(target = "step", source="problem.step") 30 | @Mapping(target = "link", source="problem.link") 31 | SearchDto toSearchDto(Review review); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/GuestBookService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.guest.GuestBook; 4 | import com.may.ars.domain.guest.GuestBookRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.List; 12 | 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | @Service 16 | public class GuestBookService { 17 | 18 | private final GuestBookRepository guestBookRepository; 19 | 20 | @Transactional(readOnly = true) 21 | public List getGuestBookList(Pageable page) { 22 | return guestBookRepository.findAllByOrderByCreatedDateDesc(page); 23 | } 24 | 25 | @Transactional(readOnly = true) 26 | public long getGuestBookCount() { 27 | return guestBookRepository.count(); 28 | } 29 | 30 | @Transactional 31 | public void saveGuestBook(GuestBook guestBook) { 32 | log.info("[" + guestBook.getNickname() + "] " + guestBook.getContent()); 33 | guestBookRepository.save(guestBook); 34 | } 35 | 36 | @Transactional 37 | public void deleteGuestBook(Long guestId) { 38 | guestBookRepository.deleteById(guestId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/message/SuccessMessage.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.message; 2 | 3 | public class SuccessMessage { 4 | 5 | public static String SUCCESS_ISSUE_TOKEN = "토큰 발급 성공입니다."; 6 | 7 | public static String SUCCESS_REGISTER_PROBLEM = "문제 등록 성공입니다."; 8 | public static String SUCCESS_UPDATE_PROBLEM = "문제 수정 성공입니다."; 9 | public static String SUCCESS_DELETE_PROBLEM = "문제 삭제 성공입니다."; 10 | public static String SUCCESS_GET_PROBLEM = "문제 상세 조회 성공입니다."; 11 | public static String SUCCESS_GET_PROBLEM_LIST = "문제 리스트 조회 성공입니다."; 12 | public static String SUCCESS_GET_PROBLEM_COUNT = "문제 개수 조회 성공입니다."; 13 | 14 | public static String SUCCESS_REGISTER_REVIEW = "리뷰 등록 성공입니다."; 15 | public static String SUCCESS_UPDATE_REVIEW = "리뷰 수정 성공입니다."; 16 | public static String SUCCESS_DELETE_REVIEW = "리뷰 삭제 성공입니다."; 17 | 18 | public static String SUCCESS_REGISTER_GUEST = "방명록 등록 성공입니다."; 19 | public static String SUCCESS_GET_GUEST_LIST = "방명록 목록 조회 성공입니다."; 20 | public static String SUCCESS_GET_GUEST_COUNT = "방명록 목록 개수 조회 성공입니다."; 21 | public static String SUCCESS_DELETE_GUEST = "방명록 삭제 성공입니다."; 22 | 23 | public static String SUCCESS_GET_SEARCH_LIST = "검색 결과 조회 성공입니다."; 24 | 25 | public static String SUCCESS_GET_TAG_LIST = "태그 리스트 조회 성공입니다."; 26 | 27 | public static String SUCCESS_AUTHORIZATION = "인증 성공입니다."; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/controller/SearchApiControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.test.web.servlet.MockMvc; 9 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 10 | 11 | import static org.hamcrest.Matchers.hasSize; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 15 | 16 | @SpringBootTest 17 | @AutoConfigureMockMvc 18 | class SearchApiControllerTest { 19 | 20 | @Autowired 21 | protected MockMvc mockMvc; 22 | 23 | // @Test 24 | void 검색_페이징_테스트() throws Exception { 25 | int page = 0; 26 | mockMvc.perform( 27 | get("/api?search=하", page) 28 | .contentType(MediaType.APPLICATION_JSON) 29 | ).andExpect(status().isOk()) 30 | .andExpect(MockMvcResultMatchers.jsonPath("$.data", hasSize(10))); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/MemberApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.dto.common.ResponseDto; 4 | import com.may.ars.dto.member.GoogleTokenDto; 5 | import com.may.ars.service.OauthService; 6 | import com.may.ars.utils.auth.AuthCheck; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import javax.validation.Valid; 14 | 15 | import static com.may.ars.common.message.SuccessMessage.SUCCESS_AUTHORIZATION; 16 | import static com.may.ars.common.message.SuccessMessage.SUCCESS_ISSUE_TOKEN; 17 | 18 | @Slf4j 19 | @RequiredArgsConstructor 20 | @RestController 21 | @RequestMapping("/api/members") 22 | public class MemberApiController { 23 | 24 | private final OauthService oauthService; 25 | 26 | @AuthCheck 27 | @GetMapping("/check") 28 | public ResponseEntity checkAuth() { 29 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_AUTHORIZATION)); 30 | } 31 | 32 | @PostMapping("/google") 33 | public ResponseEntity googleLogin(@RequestBody @Valid GoogleTokenDto tokenDto) { 34 | return ResponseEntity.ok().body(ResponseDto.of( 35 | HttpStatus.OK, SUCCESS_ISSUE_TOKEN, oauthService.googleLogin(tokenDto.getAccessToken())) 36 | ); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/mapper/ProblemMapper.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.problem.Problem; 5 | import com.may.ars.dto.problem.request.ProblemRequestDto; 6 | import com.may.ars.dto.problem.response.ProblemDto; 7 | import com.may.ars.dto.problem.response.ProblemOnlyDto; 8 | import com.may.ars.dto.review.ReviewRequestDto; 9 | import org.mapstruct.Mapper; 10 | import org.mapstruct.Mapping; 11 | import org.mapstruct.factory.Mappers; 12 | 13 | @Mapper(componentModel = "spring") 14 | public interface ProblemMapper { 15 | 16 | @Mapping(target = "id", ignore = true) 17 | @Mapping(target = "writer", source = "member") 18 | @Mapping(target = "tagList", ignore = true) 19 | @Mapping(target = "reviewList", ignore = true) 20 | Problem toEntity(ProblemRequestDto requestDto, Member member); 21 | 22 | // 리뷰와 함께 업데이트할 때 Problem 으로 매핑 23 | @Mapping(target = "id", source = "id") 24 | @Mapping(target = "writer", source = "member") 25 | @Mapping(target = "tagList", ignore = true) 26 | @Mapping(target = "link", ignore = true) 27 | @Mapping(target = "step", ignore = true) 28 | @Mapping(target = "reviewList", ignore = true) 29 | @Mapping(target = "title", ignore = true) 30 | Problem toEntity(Long id, ReviewRequestDto requestDto, Member member); 31 | 32 | ProblemDto toDto(Problem problem); 33 | 34 | ProblemOnlyDto toReviewExcludeDto(Problem problem); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/review/ReviewQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.review; 2 | 3 | import com.querydsl.core.types.dsl.BooleanExpression; 4 | import com.querydsl.jpa.impl.JPAQueryFactory; 5 | import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | import static com.may.ars.domain.review.QReview.review; 11 | 12 | @Repository 13 | public class ReviewQueryRepository extends QuerydslRepositorySupport { 14 | 15 | private final JPAQueryFactory queryFactory; 16 | 17 | public ReviewQueryRepository(JPAQueryFactory queryFactory) { 18 | super(Review.class); 19 | this.queryFactory = queryFactory; 20 | } 21 | 22 | public List search(String keyword, Long reviewId, int size) { 23 | return queryFactory 24 | .selectFrom(review) 25 | .where( 26 | ltReviewId(reviewId), 27 | review.content.containsIgnoreCase(keyword).or(review.problem.title.containsIgnoreCase(keyword)) 28 | ) 29 | .orderBy(review.id.desc()) 30 | .limit(size) 31 | .fetch(); 32 | } 33 | 34 | private BooleanExpression ltReviewId(Long reviewId) { 35 | // id < 파라미터를 첫 페이지에선 사용하지 않기 위한 동적 쿼리 36 | if (reviewId.equals(0L)) { 37 | return null; // BooleanExpression 자리에 null 이 반환되면 조건문에서 자동으로 제거 38 | } 39 | return review.id.lt(reviewId); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 개인 프로젝트 2 | # ARS (Algorithm Review Service) 3 | 알고리즘 복습을 편하게 할 수 있는 "저"만의 웹서비스입니다.
4 | 태그별 문제 리스트를 조회할 수 있고, 검색을 통해 리뷰 리스트를 조회할 수 있습니다.
5 | 등록된 문제에 계속해서 복습 내용을 추가할 수 있고, 문제 상세 보기에 복습 목록이 함께 보여집니다.
6 | 복습 알림 서비스 기능으로 알림 날짜를 설정하면 해당 날짜에 Slack을 통해 복습 알림을 받을 수 있습니다.
7 | 8 | ## 🔗 Link 9 | - [ARS 웹사이트](https://ars.vercel.app/) 10 | - [프로젝트 정리 및 후기](https://velog.io/@ayoung0073/Project-ARS-%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0) 11 | - [API 명세서](https://github.com/ayoung0073/ARS-backend/wiki) 12 | - [프론트엔드 레포지토리](https://github.com/ayoung0073/ARS-frontend) 13 | 14 | 15 | ## 🛠 Architecture 16 | ![Architecture](https://images.velog.io/images/ayoung0073/post/cadb8c1a-7029-45c5-a0aa-d41da203bc28/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202021-08-08%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%205.07.20.png) 17 | 18 | 19 | ## 📝 페이지 20 | ### 메인 페이지 21 | ![](https://images.velog.io/images/ayoung0073/post/06e01bcb-a7b9-4fa6-8df0-14ca8d8a7c4b/image.png) 22 | ### 상세 페이지 23 | "저"임이 인증이 되었다면 알림 날짜와 난이도를 즉시 수정할 수 있습니다.

24 | ![](https://images.velog.io/images/ayoung0073/post/007d9a58-d5a7-4d57-908d-12cee1a376e5/image.png) 25 |

26 | 오른쪽 영역은 고정되어 있습니다.

27 | ![상세 페이지 스크롤](https://user-images.githubusercontent.com/69340410/128628683-82af6401-53a0-4181-bed9-33b04f365265.gif) 28 | 29 | ### 태그별 문제 리스트 조회 30 | ![태그별 문제 리스트 조회](https://user-images.githubusercontent.com/69340410/128629434-18159c9a-6468-48f0-a158-881a25921ff6.gif) 31 | 32 | ### 슬랙 알림 33 | ![](https://images.velog.io/images/ayoung0073/post/bf9c1032-6f6e-475f-9e66-adc2e4826e76/image.png) 34 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/SearchApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.dto.common.ResponseDto; 4 | import com.may.ars.dto.review.SearchDto; 5 | import com.may.ars.mapper.ReviewMapper; 6 | import com.may.ars.service.SearchService; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | import java.util.stream.Collectors; 17 | 18 | import static com.may.ars.common.message.SuccessMessage.SUCCESS_GET_SEARCH_LIST; 19 | 20 | @RequiredArgsConstructor 21 | @RestController 22 | @RequestMapping("/api") 23 | public class SearchApiController { 24 | 25 | private final SearchService searchService; 26 | private final ReviewMapper reviewMapper; 27 | 28 | @GetMapping 29 | public ResponseEntity search(@RequestParam(value = "search") String name, 30 | @RequestParam(defaultValue = "0") Long cursorId, 31 | @RequestParam(defaultValue = "10") int size) { 32 | List searchList = searchService.search(name, cursorId, size).stream() 33 | .map(reviewMapper::toSearchDto) 34 | .collect(Collectors.toList()); 35 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_GET_SEARCH_LIST, searchList)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/mapper/GuestMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.guest.GuestBook; 4 | import com.may.ars.dto.guest.GuestRequestDto; 5 | import com.may.ars.dto.guest.GuestResponseDto; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.is; 12 | 13 | @SpringBootTest 14 | class GuestMapperTest { 15 | 16 | @Autowired 17 | private GuestMapper guestMapper; 18 | 19 | @Test 20 | void toEntity_테스트() { 21 | // given 22 | final GuestRequestDto requestDto = GuestRequestDto.builder() 23 | .content("테스트") 24 | .nickname("닉네임") 25 | .build(); 26 | // when 27 | final GuestBook guestBook = guestMapper.toEntity(requestDto); 28 | // then 29 | assertThat(guestBook.getContent(), is(requestDto.getContent())); 30 | assertThat(guestBook.getNickname(), is(requestDto.getNickname())); 31 | } 32 | 33 | @Test 34 | void toDto_테스트() { 35 | // given 36 | final GuestBook guestBook = GuestBook.builder() 37 | .content("테스트") 38 | .nickname("닉네임") 39 | .build(); 40 | // when 41 | final GuestResponseDto responseDto = guestMapper.toDto(guestBook); 42 | // then 43 | assertThat(responseDto.getContent(), is(guestBook.getContent())); 44 | assertThat(responseDto.getNickname(), is(guestBook.getNickname())); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: com.mysql.cj.jdbc.Driver 4 | url: ENC(E5Hrpzk1rHV875OKvLlM9YJucW4xQleWsjhY9vyNh/CfPGk9lp0EIjRfDqatYsC58HgCuju8iSqSwyUz1RTjrXF7EurOGel1D02z4JVeHlaIhvu+mq7v8D14g6ADeGbjpeLdTEfo3NvgrUH2i/lV9il722HVhLauya9gCVjr47NdQygN9cTUKSs1tVmvqTJy) 5 | username: ENC(qsyMy9CiT70aCPAEWD+/I28oBrl3KE35T6VgRE/7Mu0=) 6 | password: ENC(jo3qNQVDUro0jMsUtVEWWu3lST+hyZJu9iM07jyzErY=) 7 | 8 | jpa: 9 | open-in-view: true 10 | hibernate: 11 | ddl-auto: update 12 | show-sql: false 13 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 14 | 15 | redis: 16 | host: redis 17 | port: 6379 18 | 19 | social: 20 | google: 21 | client_id: ENC(fclV7ETZQu0c0hnldBRUASAE1EDOmh3YhZBwBW8XL99myiHBOD00l5RImeXQW3LunrI/aK8lD/N60XpPDMYFqJfTPe7UibfdKJlnH1xlr4ZtDnOXCRAHe+6sBGYwyPPe) 22 | secret_key: ENC(YGBUhpdC0V2o/Nw6j158ro5CytHA7lBJ/gluwuThNLwh6JJ/KBo/xogM0ONxwoM3) 23 | redirect_uri: http://127.0.0.1:8080/user/google/callback 24 | token_request_url: https://oauth2.googleapis.com/token 25 | profile_request_url: https://www.googleapis.com/oauth2/v3/userinfo 26 | 27 | ars: 28 | secret_key: ENC(8Z2UCA/g7q8tZxffv4INyBJBElnEQidcXWGGpGh5SHrtE8d4RzPvOCF8NGFQ1i4+PvmB0L9uFqx4v1kOC0UuBUB4ZbJnmwaSTmQjk/BRbDbba61pYkB/VXbvza31WgSyh8FCeqgqtCDrZHYplQO45Dg19KI6ROT3jX86upQEvClyYQ3a6MP4NWt8r/l4di/spDWW2WMHTudkQv0lPx3WUA==) 29 | 30 | slack: 31 | token: ENC(BYZ88wbNdP3e8h0edet3e8HH8TIo4LY83hK2s141TcqW9lNbeTYOBgsxbG15GSphSfgF3iAhm5z9GiVW8uPxVE+XLqZngoOfaGz9TW7s4ic=) 32 | 33 | logging: 34 | slack: 35 | webhook-uri: https://hooks.slack.com/services/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 36 | config: classpath:logback-spring.xml -------------------------------------------------------------------------------- /src/main/resources/application-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | driver-class-name: com.mysql.cj.jdbc.Driver 4 | url: ENC(E5Hrpzk1rHV875OKvLlM9YJucW4xQleWsjhY9vyNh/CfPGk9lp0EIjRfDqatYsC58HgCuju8iSqSwyUz1RTjrXF7EurOGel1D02z4JVeHlaIhvu+mq7v8D14g6ADeGbjpeLdTEfo3NvgrUH2i/lV9il722HVhLauya9gCVjr47NdQygN9cTUKSs1tVmvqTJy) 5 | username: ENC(qsyMy9CiT70aCPAEWD+/I28oBrl3KE35T6VgRE/7Mu0=) 6 | password: ENC(jo3qNQVDUro0jMsUtVEWWu3lST+hyZJu9iM07jyzErY=) 7 | jpa: 8 | open-in-view: true 9 | hibernate: 10 | ddl-auto: update 11 | show-sql: true 12 | database-platform: org.hibernate.dialect.MySQL5InnoDBDialect 13 | redis: 14 | host: localhost 15 | port: 6379 16 | 17 | 18 | social: 19 | google: 20 | client_id: ENC(fclV7ETZQu0c0hnldBRUASAE1EDOmh3YhZBwBW8XL99myiHBOD00l5RImeXQW3LunrI/aK8lD/N60XpPDMYFqJfTPe7UibfdKJlnH1xlr4ZtDnOXCRAHe+6sBGYwyPPe) 21 | secret_key: ENC(YGBUhpdC0V2o/Nw6j158ro5CytHA7lBJ/gluwuThNLwh6JJ/KBo/xogM0ONxwoM3) 22 | redirect_uri: http://127.0.0.1:8080/user/google/callback 23 | token_request_url: https://oauth2.googleapis.com/token 24 | profile_request_url: https://www.googleapis.com/oauth2/v3/userinfo 25 | 26 | ars: 27 | secret_key: ENC(8Z2UCA/g7q8tZxffv4INyBJBElnEQidcXWGGpGh5SHrtE8d4RzPvOCF8NGFQ1i4+PvmB0L9uFqx4v1kOC0UuBUB4ZbJnmwaSTmQjk/BRbDbba61pYkB/VXbvza31WgSyh8FCeqgqtCDrZHYplQO45Dg19KI6ROT3jX86upQEvClyYQ3a6MP4NWt8r/l4di/spDWW2WMHTudkQv0lPx3WUA==) 28 | 29 | slack: 30 | token: ENC(BYZ88wbNdP3e8h0edet3e8HH8TIo4LY83hK2s141TcqW9lNbeTYOBgsxbG15GSphSfgF3iAhm5z9GiVW8uPxVE+XLqZngoOfaGz9TW7s4ic=) 31 | 32 | logging: 33 | slack: 34 | webhook-uri: https://hooks.slack.com/services/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 35 | config: classpath:logback-spring.xml -------------------------------------------------------------------------------- /src/main/java/com/may/ars/config/RedisCacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.cache.CacheKeyPrefix; 7 | import org.springframework.data.redis.cache.RedisCacheConfiguration; 8 | import org.springframework.data.redis.cache.RedisCacheManager; 9 | import org.springframework.data.redis.connection.RedisConnectionFactory; 10 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 11 | import org.springframework.data.redis.serializer.RedisSerializationContext; 12 | import org.springframework.data.redis.serializer.StringRedisSerializer; 13 | 14 | import java.time.Duration; 15 | 16 | @RequiredArgsConstructor 17 | @Configuration 18 | public class RedisCacheConfig { 19 | 20 | @Bean(name = "cacheManager") 21 | public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { 22 | 23 | RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig() 24 | .disableCachingNullValues() 25 | .computePrefixWith(CacheKeyPrefix.simple()) 26 | .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) 27 | .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) 28 | .entryTtl(Duration.ofSeconds(30)); 29 | 30 | return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory) 31 | .cacheDefaults(configuration).build(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/utils/auth/AuthCheckAspect.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.utils.auth; 2 | 3 | import com.may.ars.common.advice.exception.UserAuthenticationException; 4 | import com.may.ars.dto.member.JwtPayload; 5 | import com.may.ars.domain.member.Member; 6 | import com.may.ars.domain.member.MemberRepository; 7 | import com.may.ars.service.JwtService; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.aspectj.lang.ProceedingJoinPoint; 11 | import org.aspectj.lang.annotation.Around; 12 | import org.aspectj.lang.annotation.Aspect; 13 | import org.springframework.stereotype.Component; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import java.util.Optional; 17 | 18 | @Slf4j 19 | @Aspect 20 | @RequiredArgsConstructor 21 | @Component 22 | public class AuthCheckAspect { 23 | 24 | private static final String AUTHORIZATION = "Authorization"; 25 | 26 | private final JwtService jwtService; 27 | private final MemberRepository memberRepository; 28 | private final HttpServletRequest httpServletRequest; 29 | 30 | @Around("@annotation(com.may.ars.utils.auth.AuthCheck)") 31 | public Object loginCheck(ProceedingJoinPoint pjp) throws Throwable { 32 | 33 | String token = httpServletRequest.getHeader(AUTHORIZATION); 34 | 35 | JwtPayload payload = jwtService.getPayload(token); 36 | log.info("AuthCheck(email) : " + payload.getEmail()); 37 | 38 | Optional optionalMember = memberRepository.findByEmail(payload.getEmail()); 39 | if(optionalMember.isEmpty()) { 40 | throw new UserAuthenticationException(); 41 | } 42 | 43 | MemberContext.currentMember.set(optionalMember.get()); 44 | return pjp.proceed(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/Problem.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import com.may.ars.domain.BaseEntity; 4 | import com.may.ars.domain.member.Member; 5 | import com.may.ars.domain.review.Review; 6 | import lombok.*; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDate; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import static java.util.Collections.singletonList; 14 | 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor 17 | @Getter 18 | @Entity 19 | @Builder 20 | public class Problem extends BaseEntity { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | @Column(name = "problem_id", updatable = false) 25 | private Long id; 26 | 27 | @ManyToOne 28 | @JoinColumn(name = "member_id", updatable = false) 29 | private Member writer; 30 | 31 | private String title; 32 | 33 | private String link; 34 | 35 | private int step; 36 | 37 | private LocalDate notificationDate; 38 | 39 | @Builder.Default 40 | @OrderBy("createdDate desc") 41 | @OneToMany(mappedBy = "problem", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 42 | private List reviewList = new ArrayList<>(); 43 | 44 | @Builder.Default 45 | @OneToMany(mappedBy = "problem", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 46 | private List tagList = new ArrayList<>(); 47 | 48 | public void setReviewAndTagList(Review review, List tagList) { 49 | this.reviewList = singletonList(review); 50 | this.tagList = tagList; 51 | } 52 | 53 | public void updateStep(int step) { 54 | this.step = step; 55 | } 56 | 57 | public void updateNotificationDate(LocalDate notificationDate) { 58 | this.notificationDate = notificationDate; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/dto/guest/GuestRequestDtoTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.guest; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.validation.ConstraintViolation; 8 | import javax.validation.Validation; 9 | import javax.validation.Validator; 10 | import javax.validation.ValidatorFactory; 11 | 12 | import java.util.Set; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | class GuestRequestDtoTest { 17 | 18 | private static ValidatorFactory factory; 19 | private static Validator validator; 20 | 21 | @BeforeAll 22 | public static void init() { 23 | factory = Validation.buildDefaultValidatorFactory(); 24 | validator = factory.getValidator(); 25 | } 26 | 27 | @AfterAll 28 | public static void close() { 29 | factory.close(); 30 | } 31 | 32 | @Test 33 | void 빈칸_유효성X_테스트() { 34 | 35 | // given 36 | GuestRequestDto requestDto = GuestRequestDto.builder() 37 | .nickname(" ") 38 | .content("테스트") 39 | .build(); 40 | 41 | // when 42 | Set> violations = validator.validate(requestDto); // 유효하지 않은 경우 violations 값을 가지고 있다. 43 | 44 | // then 45 | assertThat(violations).isNotEmpty(); 46 | 47 | } 48 | 49 | @Test 50 | void 빈칸_유효성O_테스트() { 51 | 52 | // given 53 | GuestRequestDto requestDto = GuestRequestDto.builder() 54 | .nickname("테스트") 55 | .content("테스트") 56 | .build(); 57 | 58 | // when 59 | Set> violations = validator.validate(requestDto); // 유효하지 않은 경우 violations 값을 가지고 있다. 60 | 61 | // then 62 | assertThat(violations).isEmpty(); 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/domain/problem/ProblemQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import com.querydsl.core.types.dsl.BooleanExpression; 4 | import com.querydsl.jpa.impl.JPAQueryFactory; 5 | import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import static com.may.ars.domain.problem.QProblem.problem; 9 | import static com.may.ars.domain.problem.QProblemTag.problemTag; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.List; 13 | 14 | @Repository 15 | public class ProblemQueryRepository extends QuerydslRepositorySupport { 16 | 17 | private final JPAQueryFactory queryFactory; 18 | 19 | public ProblemQueryRepository(JPAQueryFactory queryFactory) { 20 | super(Problem.class); 21 | this.queryFactory = queryFactory; 22 | } 23 | 24 | public List findAllByTag(LocalDateTime modifiedDate, Long problemId, String tagName, int size) { 25 | return queryFactory 26 | .selectFrom(problem) 27 | .join(problemTag).on(problemTag.problem.eq(problem)) 28 | .where( 29 | beforeModifiedDateAndNotProblemId(modifiedDate, problemId), 30 | problemTag.tag.tagName.eq(tagName) 31 | ) 32 | .orderBy(problem.id.desc()) 33 | .limit(size) 34 | .fetch(); 35 | } 36 | 37 | private BooleanExpression beforeModifiedDateAndNotProblemId(LocalDateTime modifiedDate, Long problemId) { 38 | // id < 파라미터를 첫 페이지에선 사용하지 않기 위한 동적 쿼리 39 | if (modifiedDate == null) { 40 | return null; // BooleanExpression 자리에 null 이 반환되면 조건문에서 자동으로 제거 41 | } 42 | if (problemId.equals(0L)) { 43 | return problem.modifiedDate.before(modifiedDate); 44 | } 45 | return problem.modifiedDate.before(modifiedDate).and(problem.id.notIn(problemId)); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Build & Deploy to AWS EC2 5 | 6 | on: 7 | push: 8 | branches: [ develop ] 9 | pull_request: 10 | branches: [ develop ] 11 | env: 12 | S3_BUCKET_NAME: ayo-springboot-build 13 | 14 | jobs: 15 | build: 16 | # 실행 환경 지정 17 | runs-on: ubuntu-18.04 18 | # Task의 sequence를 명시한다. 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up JDK 11 22 | uses: actions/setup-java@v2 23 | with: 24 | java-version: '11' 25 | distribution: 'adopt' 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | # Build 29 | - name: Build with Gradle 30 | run: ./gradlew clean build 31 | # 전송할 파일을 담을 디렉토리 생성 32 | - name: Make Directory for deliver 33 | run: mkdir deploy 34 | 35 | # Jar 파일 Copy 36 | - name: Copy Jar 37 | run: cp ./build/libs/*.jar ./deploy/ 38 | 39 | # script file Copy 40 | - name: Copy shell scripts 41 | run: cp ./scripts/* ./deploy/ 42 | 43 | # appspec.yml Copy 44 | - name: Copy appspec 45 | run: cp appspec.yml ./deploy/ 46 | 47 | # 압축파일 형태로 전달 48 | - name: Make zip file 49 | run: zip -r -qq -j ./ars-build.zip ./deploy/ 50 | 51 | # S3 Bucket으로 copy 52 | - name: Deliver to AWS S3 53 | env: 54 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 55 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 56 | run: aws s3 cp --region ap-northeast-2 --acl private ./ars-build.zip s3://$S3_BUCKET_NAME/ 57 | # Deploy 58 | - name: Deploy 59 | env: 60 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 61 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 62 | run: aws deploy create-deployment --application-name ayo-springboot-service --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name ayo-springboot-webservice-group --file-exists-behavior OVERWRITE --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=ars-build.zip --region ap-northeast-2 63 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/ReviewService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.common.advice.exception.EntityNotFoundException; 4 | import com.may.ars.common.advice.exception.UserAuthenticationException; 5 | import com.may.ars.domain.member.Member; 6 | import com.may.ars.domain.review.Review; 7 | import com.may.ars.dto.review.ReviewRequestDto; 8 | import com.may.ars.domain.problem.Problem; 9 | import com.may.ars.domain.problem.ProblemRepository; 10 | import com.may.ars.domain.review.ReviewRepository; 11 | import com.may.ars.mapper.ReviewMapper; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | @RequiredArgsConstructor 17 | @Service 18 | public class ReviewService { 19 | 20 | private final ReviewMapper reviewMapper; 21 | 22 | private final ProblemRepository problemRepository; 23 | private final ReviewRepository reviewRepository; 24 | 25 | @Transactional 26 | public void registerReview(Long problemId, ReviewRequestDto registerDto, Member member) { 27 | Problem problem = problemRepository.findById(problemId).orElseThrow(EntityNotFoundException::new); 28 | problem.updateNotificationDate(registerDto.getNotificationDate()); 29 | if (!problem.getWriter().getId().equals(member.getId())) { 30 | throw new UserAuthenticationException(); 31 | } 32 | problemRepository.save(problem); 33 | reviewRepository.save(reviewMapper.toEntity(problem, registerDto)); 34 | } 35 | 36 | @Transactional 37 | public void updateReview(Long reviewId, Review review, Member member) { 38 | checkValidUser(reviewId, member); 39 | reviewRepository.save(review); 40 | } 41 | 42 | @Transactional 43 | public void deleteReview(Long reviewId, Member member) { 44 | checkValidUser(reviewId, member); 45 | reviewRepository.deleteById(reviewId); 46 | } 47 | 48 | @Transactional(readOnly = true) 49 | public Review getReview(Long reviewId) { 50 | return reviewRepository.findById(reviewId).orElseThrow(EntityNotFoundException::new); 51 | } 52 | 53 | private void checkValidUser(Long reviewId, Member member) { 54 | reviewRepository.findReviewByIdAndMemberId(reviewId, member.getId()).orElseThrow(EntityNotFoundException::new); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/ReviewApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.dto.common.ResponseDto; 5 | import com.may.ars.dto.review.ReviewRequestDto; 6 | import com.may.ars.mapper.ReviewMapper; 7 | import com.may.ars.service.ProblemService; 8 | import com.may.ars.service.ReviewService; 9 | import com.may.ars.utils.auth.AuthCheck; 10 | import com.may.ars.utils.auth.MemberContext; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.validation.Valid; 17 | 18 | import static com.may.ars.common.message.SuccessMessage.*; 19 | 20 | @RequiredArgsConstructor 21 | @RestController 22 | @RequestMapping("/api") 23 | public class ReviewApiController { 24 | 25 | private final ProblemService problemService; 26 | private final ReviewService reviewService; 27 | 28 | private final ReviewMapper reviewMapper; 29 | 30 | @AuthCheck 31 | @PostMapping("/problems/{problemId}/reviews") 32 | public ResponseEntity saveReview(@PathVariable("problemId") Long problemId, @RequestBody @Valid ReviewRequestDto requestDto) { 33 | Member member = MemberContext.currentMember.get(); 34 | 35 | reviewService.registerReview(problemId, requestDto, member); 36 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_REGISTER_REVIEW)); 37 | } 38 | 39 | @AuthCheck 40 | @PutMapping("/problems/{problemId}/reviews/{reviewId}") 41 | public ResponseEntity updateReview(@PathVariable("problemId") Long problemId, 42 | @PathVariable("reviewId") Long reviewId, 43 | @RequestBody @Valid ReviewRequestDto requestDto) { 44 | Member member = MemberContext.currentMember.get(); 45 | problemService.updateNotificationDate(problemId, member, requestDto.getNotificationDate()); 46 | reviewService.updateReview(reviewId, reviewMapper.toEntity(reviewId, requestDto), member); 47 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_UPDATE_REVIEW)); 48 | } 49 | 50 | @AuthCheck 51 | @DeleteMapping("/reviews/{reviewId}") 52 | public ResponseEntity deleteReview(@PathVariable("reviewId") Long reviewId) { 53 | Member member = MemberContext.currentMember.get(); 54 | 55 | reviewService.deleteReview(reviewId, member); 56 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_DELETE_REVIEW)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/mapper/ProblemMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.problem.Problem; 5 | import com.may.ars.dto.problem.request.ProblemRequestDto; 6 | import com.may.ars.dto.problem.response.ProblemDto; 7 | import com.may.ars.dto.problem.response.ProblemOnlyDto; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | import java.time.LocalDate; 13 | 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.is; 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | @SpringBootTest 19 | class ProblemMapperTest { 20 | 21 | @Autowired 22 | private ProblemMapper problemMapper; 23 | 24 | @Test 25 | void toEntity_테스트() { 26 | // given 27 | final ProblemRequestDto registerDto = ProblemRequestDto.builder() 28 | .content("hi") 29 | .title("hi") 30 | .step(3) 31 | .build(); 32 | final Member member = null; 33 | // when 34 | final Problem problem = problemMapper.toEntity(registerDto, member); 35 | // then 36 | assertNotNull(problem); 37 | assertThat(problem.getTitle(), is(registerDto.getTitle())); 38 | } 39 | 40 | @Test 41 | void 리뷰목록_포함_toDto_테스트() { 42 | // given 43 | final Problem problem = Problem.builder() 44 | .writer(null) 45 | .link("test.com") 46 | .title("test") 47 | .tagList(null) 48 | .notificationDate(LocalDate.of(2021, 7, 3)) 49 | .reviewList(null) 50 | .build(); 51 | // when 52 | final ProblemDto problemDto = problemMapper.toDto(problem); 53 | // given 54 | assertThat(problemDto.getTitle(), is(problem.getTitle())); 55 | assertThat(problemDto.getReviewList(), is(problem.getReviewList())); 56 | } 57 | 58 | @Test 59 | void 리뷰목록_포함X_toDto_테스트() { 60 | // given 61 | final Problem problem = Problem.builder() 62 | .writer(null) 63 | .link("test.com") 64 | .title("test") 65 | .tagList(null) 66 | .notificationDate(LocalDate.of(2021, 7, 3)) 67 | .reviewList(null) 68 | .build(); 69 | // when 70 | final ProblemOnlyDto problemDto = problemMapper.toReviewExcludeDto(problem); 71 | // given 72 | assertThat(problemDto.getTitle(), is(problem.getTitle())); 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | INFO 12 | 13 | 14 | 15 | 16 | [%date] %highlight([%level]) [%logger{10} %file:%line] %msg%n 17 | 18 | 19 | 20 | ars-log 21 | 22 | 23 | ars/ 24 | 25 | 26 | ap-northeast-2 27 | 28 | 29 | 30 | 50 31 | 32 | 33 | 34 | 30000 35 | 36 | 37 | 38 | 39 | 5000 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | [%date] %highlight([%level]) [%logger{10} %file:%line] %msg%n 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.may.ars.common.advice.exception.JsonWriteException; 6 | import com.may.ars.dto.member.JwtPayload; 7 | import io.jsonwebtoken.Claims; 8 | import io.jsonwebtoken.Jwts; 9 | import io.jsonwebtoken.MalformedJwtException; 10 | import io.jsonwebtoken.SignatureAlgorithm; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.crypto.spec.SecretKeySpec; 16 | import javax.xml.bind.DatatypeConverter; 17 | import java.security.Key; 18 | import java.util.Date; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | @Slf4j 23 | @Service 24 | public class JwtService { 25 | 26 | private static final ObjectMapper objectMapper = new ObjectMapper(); 27 | 28 | @Value("${ars.secret_key}") 29 | private String SECRET_KEY; 30 | 31 | public String createToken(JwtPayload payload) { 32 | SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; 33 | Date expireTime = new Date(); 34 | expireTime.setTime(expireTime.getTime() + 1000 * 60 * 30 * 60); // 30m * 60 35 | byte[] secretKeyBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); 36 | 37 | Map headerMap = new HashMap<>(); 38 | 39 | headerMap.put("typ", "JWT"); 40 | headerMap.put("alg", "HS256"); 41 | 42 | Key signingKey = new SecretKeySpec(secretKeyBytes, signatureAlgorithm.getJcaName()); 43 | 44 | try { 45 | return Jwts.builder() 46 | .setHeader(headerMap) 47 | .setSubject(objectMapper.writeValueAsString(payload)) 48 | .setExpiration(expireTime) 49 | .signWith(signingKey, signatureAlgorithm) 50 | .compact(); 51 | } catch (JsonProcessingException e) { 52 | throw new JsonWriteException(); 53 | } 54 | } 55 | 56 | public JwtPayload getPayload(String token) { 57 | try { 58 | Claims claims = Jwts.parserBuilder() 59 | .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY)) 60 | .build() 61 | .parseClaimsJws(token) 62 | .getBody(); 63 | 64 | return objectMapper.readValue(claims.getSubject(), JwtPayload.class); 65 | } catch (JsonProcessingException | IllegalArgumentException | MalformedJwtException e) { 66 | throw new JsonWriteException(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/GuestApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.dto.common.ResponseDto; 4 | import com.may.ars.dto.guest.GuestRequestDto; 5 | import com.may.ars.dto.guest.GuestResponseDto; 6 | import com.may.ars.mapper.GuestMapper; 7 | import com.may.ars.service.GuestBookService; 8 | import com.may.ars.utils.auth.AuthCheck; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import javax.validation.Valid; 16 | import java.util.List; 17 | 18 | import static com.may.ars.common.message.SuccessMessage.*; 19 | import static java.util.stream.Collectors.toList; 20 | 21 | @RequiredArgsConstructor 22 | @RestController 23 | @RequestMapping("/api/guests") 24 | public class GuestApiController { 25 | 26 | private final GuestBookService guestBookService; 27 | private final GuestMapper guestMapper; 28 | 29 | @GetMapping 30 | public ResponseEntity getGuestBookList(@RequestParam(value = "page", defaultValue = "0") int page, 31 | @RequestParam(value = "size", defaultValue = "10") int size) { 32 | List guestBookList = guestBookService.getGuestBookList(PageRequest.of(page, size)).stream() 33 | .map(guestMapper::toDto) 34 | .collect(toList()); 35 | return ResponseEntity.ok().body(ResponseDto.of( 36 | HttpStatus.OK, SUCCESS_GET_GUEST_LIST, guestBookList) 37 | ); 38 | } 39 | 40 | @GetMapping("/count") 41 | public ResponseEntity getGuestBookCount() { 42 | return ResponseEntity.ok().body(ResponseDto.of( 43 | HttpStatus.OK, SUCCESS_GET_GUEST_COUNT, guestBookService.getGuestBookCount()) 44 | ); 45 | } 46 | 47 | @PostMapping 48 | public ResponseEntity saveGuestBook(@RequestBody @Valid GuestRequestDto requestDto) { 49 | guestBookService.saveGuestBook(guestMapper.toEntity(requestDto)); 50 | return ResponseEntity.ok().body(ResponseDto.of( 51 | HttpStatus.OK, SUCCESS_REGISTER_GUEST) 52 | ); 53 | } 54 | 55 | @AuthCheck 56 | @DeleteMapping("/{guestId}") 57 | public ResponseEntity deleteGuestBook(@PathVariable Long guestId) { 58 | guestBookService.deleteGuestBook(guestId); 59 | return ResponseEntity.ok().body(ResponseDto.of( 60 | HttpStatus.OK, SUCCESS_DELETE_GUEST) 61 | ); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/mapper/ReviewMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.mapper; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.problem.Problem; 5 | import com.may.ars.domain.review.Review; 6 | import com.may.ars.dto.problem.request.ProblemRequestDto; 7 | import com.may.ars.dto.review.SearchDto; 8 | import com.may.ars.dto.review.ReviewRequestDto; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.is; 15 | import static org.junit.jupiter.api.Assertions.*; 16 | 17 | @SpringBootTest 18 | class ReviewMapperTest { 19 | 20 | @Autowired 21 | private ProblemMapper problemMapper; 22 | 23 | @Autowired 24 | private ReviewMapper reviewMapper; 25 | 26 | @Test 27 | void 등록_toEntity_테스트() { 28 | // given 29 | final ProblemRequestDto registerDto = ProblemRequestDto.builder() 30 | .content("hi") 31 | .title("hi") 32 | .step(3) 33 | .build(); 34 | final Member member = null; 35 | final Problem problem = problemMapper.toEntity(registerDto, member); 36 | 37 | // when 38 | final Review review = reviewMapper.toEntity(problem, registerDto); 39 | 40 | //then 41 | assertNotNull(review); 42 | assertThat(review.getContent(), is(registerDto.getContent())); 43 | } 44 | 45 | @Test 46 | void 수정_toEntity_테스트() { 47 | // given 48 | final ReviewRequestDto requestDto = ReviewRequestDto.builder() 49 | .content("hi") 50 | .build(); 51 | 52 | // when 53 | final Review review = reviewMapper.toEntity(1L, requestDto); 54 | 55 | //then 56 | assertNotNull(review); 57 | assertThat(review.getContent(), is(requestDto.getContent())); 58 | } 59 | 60 | @Test 61 | void toSearchDto_테스트() { 62 | // given 63 | Problem problem = Problem.builder() 64 | .title("테스트") 65 | .link("test.com") 66 | .step(3) 67 | .build(); 68 | Review review = Review.builder() 69 | .content("내용 테스트") 70 | .problem(problem) 71 | .build(); 72 | 73 | // when 74 | SearchDto searchDto = reviewMapper.toSearchDto(review); 75 | 76 | // then 77 | assertThat(searchDto.getTitle(), is(problem.getTitle())); 78 | assertThat(searchDto.getStep(), is(problem.getStep())); 79 | assertThat(searchDto.getLink(), is(problem.getLink())); 80 | assertThat(searchDto.getContent(), is(review.getContent())); 81 | assertThat(searchDto.getCreatedDate(), is(review.getCreatedDate())); 82 | assertThat(searchDto.getId(), is(problem.getId())); 83 | } 84 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/domain/problem/ProblemQueryRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.is; 14 | 15 | @SpringBootTest 16 | @Transactional 17 | class ProblemQueryRepositoryTest { 18 | 19 | @Autowired 20 | private ProblemRepository problemRepository; 21 | 22 | @Autowired 23 | private ProblemQueryRepository problemQueryRepository; 24 | 25 | private final Tag tag1 = new Tag("테스트1"); 26 | private final Tag tag2 = new Tag("테스트2"); 27 | 28 | private Problem problem1; 29 | private Problem problem2; 30 | private Problem problem3; 31 | 32 | @BeforeEach 33 | void init() { 34 | 35 | String title1 = "테스트요1"; 36 | String title2 = "테스트요2"; 37 | String title3 = "테스트요3"; 38 | 39 | problem1 = Problem.builder() 40 | .title(title1) 41 | .build(); 42 | 43 | problem2 = Problem.builder() 44 | .title(title2) 45 | .build(); 46 | 47 | problem3 = Problem.builder() 48 | .title(title3) 49 | .build(); 50 | 51 | problem1.getTagList().add(new ProblemTag(problem1, tag1)); 52 | problem2.getTagList().add(new ProblemTag(problem2, tag1)); 53 | problem2.getTagList().add(new ProblemTag(problem2, tag2)); 54 | problem3.getTagList().add(new ProblemTag(problem3, tag1)); 55 | problem3.getTagList().add(new ProblemTag(problem3, tag2)); 56 | 57 | // when 58 | problemRepository.save(problem1); 59 | problemRepository.save(problem2); 60 | problemRepository.save(problem3); 61 | 62 | } 63 | 64 | @Test 65 | void 최신_업데이트순으로_태그리스트_조회() { 66 | List problemList = problemQueryRepository.findAllByTag(LocalDateTime.now(), 0L, tag1.getTagName(), 5); 67 | assertThat(problemList.size(), is(3)); 68 | assertThat(problemList.get(0), is(problem3)); 69 | assertThat(problemList.get(1), is(problem2)); 70 | assertThat(problemList.get(2), is(problem1)); 71 | } 72 | 73 | @Test 74 | void 입력된_수정된_날짜_이전_최신업데이트_태그리스트_조회() { 75 | List problemList = problemQueryRepository.findAllByTag(problem3.getModifiedDate(), 0L, tag2.getTagName(), 5); 76 | assertThat(problemList.size(), is(1)); 77 | assertThat(problemList.get(0), is(problem2)); 78 | } 79 | 80 | @Test 81 | void 입력된_수정된_날짜_이전_id_중복제거_최신업데이트_태그리스트_조회() { 82 | List problemList = problemQueryRepository.findAllByTag(problem3.getModifiedDate(), problem3.getId(), tag2.getTagName(), 5); 83 | assertThat(problemList.size(), is(1)); 84 | assertThat(problemList.get(0), is(problem2)); 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/OauthService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.may.ars.common.advice.exception.JsonWriteException; 7 | import com.may.ars.common.advice.exception.JwtException; 8 | import com.may.ars.config.properties.GoogleProperties; 9 | import com.may.ars.dto.member.JwtPayload; 10 | import com.may.ars.dto.member.LoginSuccessDto; 11 | import com.may.ars.domain.member.Member; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.*; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.util.MultiValueMap; 17 | import org.springframework.web.client.RestTemplate; 18 | 19 | import java.util.Optional; 20 | 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | @Service 24 | public class OauthService { 25 | 26 | private final RestTemplate restTemplate; 27 | private final ObjectMapper objectMapper; 28 | 29 | private final MemberService memberService; 30 | private final JwtService jwtService; 31 | 32 | private final GoogleProperties googleProperties; 33 | 34 | public LoginSuccessDto googleLogin(String accessToken) { 35 | JsonNode profile = getProfile(accessToken, googleProperties.getProfileRequestUrl()); 36 | String email = profile.get("email").textValue(); 37 | Optional optional = memberService.findMemberByEmail(email); 38 | Member member; 39 | 40 | if (optional.isEmpty()) { // 로그인 불가 41 | log.info("[GUEST 로그인] " + email); 42 | throw new JwtException(); 43 | } else { // 로그인 44 | log.info("[USER 로그인] " + email); 45 | member = optional.get(); 46 | 47 | String token = jwtService.createToken(new JwtPayload(member.getId(), email)); 48 | 49 | return LoginSuccessDto.builder() 50 | .nickname(member.getNickname()) 51 | .access_token(token) 52 | .build(); 53 | } 54 | } 55 | 56 | private JsonNode getProfile(String accessToken, String profileRequestUrl) { 57 | HttpHeaders headers = new HttpHeaders(); 58 | 59 | headers.setContentType(MediaType.APPLICATION_JSON); 60 | headers.setBearerAuth(accessToken); 61 | 62 | HttpEntity> profileRequest = new HttpEntity<>(headers); 63 | 64 | ResponseEntity restResponse = restTemplate.exchange( 65 | profileRequestUrl, 66 | HttpMethod.POST, 67 | profileRequest, 68 | String.class 69 | ); 70 | log.info(restResponse.getBody()); 71 | return readBody(restResponse.getBody()); 72 | } 73 | 74 | private JsonNode readBody(String responseBody) { 75 | try { 76 | return objectMapper.readTree(responseBody); 77 | } catch (JsonProcessingException e) { 78 | throw new JsonWriteException(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/dto/problem/request/ProblemRequestDtoTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.dto.problem.request; 2 | 3 | import org.junit.jupiter.api.AfterAll; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.validation.ConstraintViolation; 8 | import javax.validation.Validation; 9 | import javax.validation.Validator; 10 | import javax.validation.ValidatorFactory; 11 | 12 | import java.time.LocalDate; 13 | import java.util.ArrayList; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | import static net.bytebuddy.matcher.ElementMatchers.is; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.junit.jupiter.api.Assertions.*; 21 | 22 | class ProblemRequestDtoTest { 23 | 24 | private static ValidatorFactory factory; 25 | private static Validator validator; 26 | 27 | @BeforeAll 28 | public static void init() { 29 | factory = Validation.buildDefaultValidatorFactory(); 30 | validator = factory.getValidator(); 31 | } 32 | 33 | @AfterAll 34 | public static void close() { 35 | factory.close(); 36 | } 37 | 38 | @Test 39 | void 리스트_NotNull_유효O_테스트() { 40 | 41 | // given 42 | ProblemRequestDto requestDto = ProblemRequestDto.builder() 43 | .title("테스트") 44 | .content("테스트") 45 | .link("ㄹㄴㅇ") 46 | .notificationDate(LocalDate.of(2021, 7, 3)) 47 | .step(3) 48 | .tagList(new ArrayList<>(Arrays.asList("테스트1", "테스트2", "테스트3"))) 49 | .build(); 50 | 51 | // when 52 | Set> violations = validator.validate(requestDto); 53 | 54 | // then 55 | assertThat(violations).isEmpty(); 56 | 57 | } 58 | 59 | @Test 60 | void 리스트_NotNull_유효X_테스트() { 61 | 62 | // given 63 | ProblemRequestDto requestDto = ProblemRequestDto.builder() 64 | .title("테스트") 65 | .content("테스트") 66 | .link("test.com") 67 | .notificationDate(LocalDate.of(2021, 7, 3)) 68 | .step(3) 69 | .tagList(null) 70 | .build(); 71 | 72 | // when 73 | Set> violations = validator.validate(requestDto); 74 | 75 | // then 76 | assertThat(violations).isNotEmpty(); 77 | 78 | } 79 | 80 | @Test 81 | void int_NotNull_유효성_테스트() { 82 | 83 | // given 84 | ProblemRequestDto requestDto = ProblemRequestDto.builder() 85 | .title("테스트") 86 | .content("테스트") 87 | .link("test.com") 88 | .notificationDate(LocalDate.of(2021, 7, 3)) 89 | .tagList(new ArrayList<>(Arrays.asList("테스트1", "테스트2", "테스트3"))) 90 | .build(); 91 | 92 | // when 93 | Set> violations = validator.validate(requestDto); 94 | 95 | // then 96 | assertThat(violations).isEmpty(); 97 | assertThat(requestDto.getStep()).isEqualTo(1); 98 | 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/SlackBotService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.may.ars.common.advice.exception.JsonWriteException; 7 | import com.may.ars.domain.problem.Problem; 8 | import com.may.ars.dto.slack.Attachment; 9 | import com.may.ars.dto.slack.Message; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.http.*; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.web.client.RestTemplate; 16 | 17 | import java.util.List; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Service 22 | public class SlackBotService { 23 | 24 | private final RestTemplate restTemplate; 25 | private final ObjectMapper objectMapper; 26 | 27 | @Value("${slack.token}") 28 | private String slackToken; 29 | 30 | public void notification(Problem problem) { 31 | String url = "https://slack.com/api/chat.postMessage"; 32 | 33 | HttpHeaders headers = new HttpHeaders(); 34 | headers.add("Authorization", "Bearer " + slackToken); 35 | headers.add("Content-type", "application/json; charset=utf-8"); 36 | 37 | Message message = Message.builder() 38 | .channel(problem.getWriter().getSlackId()) 39 | .text(problem.getTitle() + " 문제를 풀 시간입니다!") 40 | .attachments(List.of(Attachment.builder() 41 | .text("<" + problem.getLink() + "|" + problem.getTitle() + "> 문제를 푸는 시간입니다! \uD83D\uDE00 \n 리뷰를 추가하려면 를 눌러주세요.") 42 | .build()) 43 | ) 44 | .build(); 45 | 46 | HttpEntity requestEntity = new HttpEntity<>(message, headers); 47 | ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); 48 | 49 | String response = responseEntity.getBody(); 50 | log.info(response); 51 | } 52 | 53 | public void getSlackIdByEmail(String email) { 54 | String url = "https://slack.com/api/users.lookupByEmail"; 55 | url += "?email=" + email; 56 | 57 | HttpHeaders headers = new HttpHeaders(); 58 | headers.add("Authorization", "Bearer " + slackToken); 59 | headers.add("Content-type", "application/x-www-form-urlencoded"); 60 | 61 | HttpEntity requestEntity = new HttpEntity<>(headers); 62 | ResponseEntity restResponse = restTemplate.exchange( 63 | url, 64 | HttpMethod.GET, 65 | requestEntity, 66 | String.class 67 | ); 68 | 69 | int status = restResponse.getStatusCode().value(); 70 | String response = restResponse.getBody(); 71 | log.info("Response status: " + status); 72 | log.info(response); 73 | JsonNode body; 74 | try{ 75 | body = objectMapper.readTree(restResponse.getBody()); 76 | } catch (JsonProcessingException e) { 77 | throw new JsonWriteException(); 78 | } 79 | 80 | if (body.get("ok").asBoolean()) { 81 | body.get("user").get("id").textValue(); 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/domain/problem/ProblemRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.domain.problem; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.domain.member.MemberRepository; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 8 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 9 | import org.springframework.test.context.ActiveProfiles; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.hamcrest.MatcherAssert.assertThat; 14 | import static org.hamcrest.Matchers.is; 15 | 16 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 17 | @DataJpaTest 18 | class ProblemRepositoryTest { 19 | 20 | @Autowired 21 | private ProblemRepository problemRepository; 22 | 23 | @Autowired 24 | private MemberRepository memberRepository; 25 | 26 | @Test 27 | void 문제_존재X_테스트() { 28 | // given 29 | final Member member = Member.builder() 30 | .email("ayong703@gmail.com") 31 | .build(); 32 | 33 | final Problem problem = Problem.builder() 34 | .writer(member) 35 | .build(); 36 | 37 | memberRepository.save(member); 38 | problemRepository.save(problem); 39 | 40 | // when 41 | Optional optional = problemRepository.findProblemByIdAndWriter(problem.getId() + 1, member); 42 | 43 | // then 44 | assertThat(optional.isEmpty(), is(true)); 45 | } 46 | 47 | @Test 48 | void 문제_존재O_테스트() { 49 | // given 50 | final Member member = Member.builder() 51 | .email("ayong703@gmail.com") 52 | .build(); 53 | 54 | final Problem problem = Problem.builder() 55 | .writer(member) 56 | .build(); 57 | 58 | memberRepository.save(member); 59 | problemRepository.save(problem); 60 | 61 | // when 62 | Optional optional = problemRepository.findProblemByIdAndWriter(problem.getId(), member); 63 | 64 | // then 65 | assertThat(optional.isPresent(), is(true)); 66 | } 67 | 68 | @Test 69 | void 문제_삭제_테스트() { 70 | // given 71 | final Member member = Member.builder() 72 | .email("ayong703@gmail.com") 73 | .build(); 74 | 75 | final Problem problem = Problem.builder() 76 | .writer(member) 77 | .build(); 78 | 79 | memberRepository.save(member); 80 | problemRepository.save(problem); 81 | 82 | problemRepository.deleteProblemById(problem.getId()); 83 | } 84 | 85 | 86 | @Test 87 | void 문제_수정_테스트() { 88 | // given 89 | String title = "테스트"; 90 | 91 | String link = "test.com"; 92 | 93 | final Member member = Member.builder() 94 | .email("ayong703@gmail.com") 95 | .build(); 96 | final Problem problem = Problem.builder() 97 | .writer(member) 98 | .build(); 99 | 100 | 101 | memberRepository.save(member); 102 | problemRepository.save(problem); 103 | 104 | // when 105 | problem.updateStep(3); 106 | 107 | problemRepository.save(problem); 108 | 109 | // then 110 | Problem updatedProblem = problemRepository.findById(problem.getId()).get(); 111 | 112 | assertThat(updatedProblem.getStep(), is(3)); 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/java/com/may/ars/common/advice/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.common.advice; 2 | import com.may.ars.common.advice.exception.BusinessException; 3 | import com.may.ars.dto.common.ResponseDto; 4 | import io.jsonwebtoken.security.SignatureException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.http.converter.HttpMessageNotReadableException; 8 | import org.springframework.web.bind.MethodArgumentNotValidException; 9 | import org.springframework.web.bind.annotation.ControllerAdvice; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 12 | 13 | import javax.servlet.ServletException; 14 | 15 | @Slf4j 16 | @ControllerAdvice 17 | public class GlobalExceptionHandler { 18 | 19 | /* 20 | * javax.validation.Valid or @Validated 으로 binding error 발생할 경우 21 | * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못 할 경우 발생 22 | */ 23 | @ExceptionHandler(HttpMessageNotReadableException.class) 24 | protected ResponseEntity> httpMessageNotReadableException() { 25 | log.error("HttpMessageNotReadableException"); 26 | final ExceptionCode exceptionCode = ExceptionCode.INVALID_INPUT_VALUE; 27 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), exceptionCode.getMessage()), exceptionCode.getStatus()); 28 | } 29 | 30 | @ExceptionHandler(ServletException.class) 31 | protected ResponseEntity> servletException(final ServletException e) { 32 | log.error("ServletException Exception : " + e.getMessage()); 33 | final ExceptionCode exceptionCode = ExceptionCode.SERVLET_ERROR; 34 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), exceptionCode.getMessage()), exceptionCode.getStatus()); 35 | } 36 | 37 | @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) 38 | protected ResponseEntity> handleMethodArgumentException() { 39 | log.error("MethodArgumentException"); 40 | final ExceptionCode exceptionCode = ExceptionCode.INVALID_INPUT_VALUE; 41 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), exceptionCode.getMessage()), exceptionCode.getStatus()); 42 | } 43 | 44 | @ExceptionHandler(SignatureException.class) 45 | protected ResponseEntity> signatureException(SignatureException e) { 46 | log.error("SignatureException : " + e.getMessage()); 47 | final ExceptionCode exceptionCode = ExceptionCode.JWT_EXCEPTION; 48 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), exceptionCode.getMessage()), exceptionCode.getStatus()); 49 | } 50 | 51 | @ExceptionHandler(BusinessException.class) 52 | protected ResponseEntity> businessException(final BusinessException e) { 53 | log.error("Business Exception : " + e.getMessage()); 54 | final ExceptionCode exceptionCode = e.getExceptionCode(); 55 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), e.getMessage()), exceptionCode.getStatus()); 56 | } 57 | 58 | /** 59 | * 예상치 못한 예외 처리 60 | */ 61 | @ExceptionHandler(Exception.class) 62 | protected ResponseEntity> handleException(final Exception e) { 63 | log.error("Exception : " + e.getMessage()); 64 | final ExceptionCode exceptionCode = ExceptionCode.INTERNAL_SERVER_ERROR; 65 | return new ResponseEntity<>(ResponseDto.fail(exceptionCode.getStatus(), exceptionCode.getMessage()), exceptionCode.getStatus()); 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/test/java/com/may/ars/service/ReviewServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.common.advice.ExceptionCode; 4 | import com.may.ars.common.advice.exception.EntityNotFoundException; 5 | import com.may.ars.common.advice.exception.UserAuthenticationException; 6 | import com.may.ars.dto.review.ReviewRequestDto; 7 | import com.may.ars.domain.member.Member; 8 | import com.may.ars.domain.problem.Problem; 9 | import com.may.ars.domain.problem.ProblemRepository; 10 | import com.may.ars.domain.review.ReviewRepository; 11 | import com.may.ars.mapper.ReviewMapper; 12 | import org.junit.jupiter.api.DisplayName; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import java.util.Optional; 20 | 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.is; 23 | import static org.junit.jupiter.api.Assertions.assertThrows; 24 | import static org.mockito.BDDMockito.given; 25 | 26 | @ExtendWith(MockitoExtension.class) 27 | public class ReviewServiceTest { 28 | 29 | @InjectMocks 30 | private ReviewService reviewService; 31 | 32 | @Mock 33 | private ReviewRepository reviewRepository; 34 | 35 | @Mock 36 | private ProblemRepository problemRepository; 37 | 38 | @Mock 39 | private ReviewMapper reviewMapper; 40 | 41 | final Member member = Member.builder() 42 | .id(1L) 43 | .email("test") 44 | .nickname("test") 45 | .build(); 46 | 47 | void setup() { 48 | final Problem problem = Problem.builder() 49 | .writer(member) 50 | .build(); 51 | 52 | given(problemRepository.findById(1L)).willReturn(Optional.of(problem)); 53 | } 54 | 55 | @Test 56 | @DisplayName("리뷰 등록 테스트") 57 | void 리뷰_등록_성공_테스트() { 58 | setup(); 59 | // given 60 | Long problemId = 1L; 61 | 62 | final ReviewRequestDto requestDto = ReviewRequestDto.builder() 63 | .content("리뷰 등록 테스트") 64 | .build(); 65 | 66 | // when 67 | reviewService.registerReview(problemId, requestDto, member); 68 | } 69 | 70 | @Test 71 | @DisplayName("리뷰 등록 실패 테스트 - 문제 존재하지 않는 경우") 72 | void 리뷰_등록_문제X_테스트() { 73 | //given 74 | given(problemRepository.findById(2L)).willReturn(Optional.empty()); 75 | 76 | Long problemId = 2L; 77 | 78 | final ReviewRequestDto requestDto = ReviewRequestDto.builder() 79 | .content("리뷰 등록 테스트") 80 | .build(); 81 | 82 | // when 83 | EntityNotFoundException e = assertThrows(EntityNotFoundException.class, 84 | () -> reviewService.registerReview(problemId, requestDto, member)); // 예외가 발생해야 한다. 85 | 86 | //then 87 | assertThat(e.getMessage(), is(ExceptionCode.ENTITY_NOT_FOUND.getMessage())); 88 | } 89 | 90 | @Test 91 | @DisplayName("리뷰 등록 실패 테스트 - 권한 없는 경우") 92 | void 리뷰_등록_권한X_테스트() { 93 | setup(); 94 | //given 95 | final Member member = Member.builder() 96 | .build(); 97 | 98 | Long problemId = 1L; 99 | 100 | final ReviewRequestDto requestDto = ReviewRequestDto.builder() 101 | .content("리뷰 등록 테스트") 102 | .build(); 103 | 104 | // when 105 | UserAuthenticationException e = assertThrows(UserAuthenticationException.class, 106 | () -> reviewService.registerReview(problemId, requestDto, member)); // 예외가 발생해야 한다. 107 | 108 | //then 109 | assertThat(e.getMessage(), is(ExceptionCode.NOT_VALID_USER.getMessage())); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/com/may/ars/service/ProblemServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.domain.problem.*; 4 | import com.may.ars.domain.review.Review; 5 | import com.may.ars.domain.review.ReviewRepository; 6 | import com.may.ars.dto.problem.request.ProblemRequestDto; 7 | import org.junit.jupiter.api.DisplayName; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.*; 14 | 15 | import static java.util.Collections.singletonList; 16 | 17 | import static java.util.stream.Collectors.toList; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.is; 20 | 21 | @SpringBootTest 22 | public class ProblemServiceTest { 23 | 24 | @Autowired 25 | private ProblemService problemService; 26 | 27 | @Autowired 28 | private ProblemRepository problemRepository; 29 | 30 | @Autowired 31 | private ReviewRepository reviewRepository; 32 | 33 | @Autowired 34 | private TagRepository tagRepository; 35 | 36 | @Test 37 | @Transactional 38 | @DisplayName("문제, 리뷰 등록 테스트") 39 | void 문제_리뷰_등록_테스트() { 40 | Problem problem = Problem.builder().build(); 41 | Review review = Review.builder() 42 | .problem(problem) 43 | .content("테스트") 44 | .build(); 45 | 46 | problem.setReviewAndTagList(review, null); 47 | problemRepository.save(problem); 48 | 49 | Problem saveProblem = problemService.getProblemById(problem.getId()); 50 | assertThat(reviewRepository.existsById(review.getId()), is(true)); 51 | assertThat(saveProblem.getReviewList().size(), is(1)); 52 | } 53 | 54 | @Test 55 | @Transactional 56 | @DisplayName("문제, 태그 등록 테스트") 57 | void 문제_태그_등록_테스트() { 58 | Problem problem = Problem.builder().build(); 59 | List tagStringList = List.of("테스트1", "테스트2"); 60 | List tagList = tagStringList.stream().map(tag -> new ProblemTag(problem, new Tag(tag))).collect(toList()); 61 | problem.setReviewAndTagList(null, tagList); 62 | problemRepository.save(problem); 63 | 64 | Problem saveProblem = problemService.getProblemById(problem.getId()); 65 | assertThat(saveProblem.getTagList().size(), is(tagStringList.size())); 66 | } 67 | 68 | @Test 69 | @Transactional 70 | @DisplayName("문제, 태그 중복 저장 테스트") 71 | void 태그_중복_저장_테스트() { 72 | Tag saveTag = new Tag("테스트1"); 73 | tagRepository.save(saveTag); 74 | Problem problem = Problem.builder().build(); 75 | List tagStringList = List.of("테스트1", "테스트2"); 76 | List tagList = tagStringList.stream() 77 | .map(tagName -> tagRepository.findByTagName(tagName) 78 | .map( 79 | tag -> new ProblemTag(problem, tag)).orElseGet( 80 | () -> new ProblemTag(problem, new Tag(tagName)) 81 | )).collect(toList()); 82 | problem.setReviewAndTagList(null, tagList); 83 | problemRepository.save(problem); 84 | 85 | Problem saveProblem = problemService.getProblemById(problem.getId()); 86 | assertThat(saveProblem.getTagList().size(), is(tagStringList.size())); 87 | } 88 | 89 | @Test 90 | @Transactional 91 | @DisplayName("문제 저장 통합 테스트") 92 | void 문제_저장_테스트() { 93 | Tag saveTag = new Tag("테스트1"); 94 | tagRepository.save(saveTag); 95 | Problem problem = Problem.builder().build(); 96 | ArrayList tagStringList = new ArrayList<>(Arrays.asList("테스트1", "테스트2")); 97 | ProblemRequestDto requestDto = ProblemRequestDto.builder() 98 | .tagList(tagStringList) 99 | .content("테스트") 100 | .build(); 101 | problemService.registerProblem(problem, requestDto); 102 | 103 | Problem saveProblem = problemService.getProblemById(problem.getId()); 104 | assertThat(saveProblem.getTagList().size(), is(tagStringList.size())); 105 | assertThat(saveProblem.getReviewList().get(0).getContent(), is(requestDto.getContent())); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/service/ProblemService.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.service; 2 | 3 | import com.may.ars.common.advice.exception.EntityNotFoundException; 4 | import com.may.ars.domain.member.Member; 5 | import com.may.ars.domain.review.Review; 6 | import com.may.ars.dto.problem.request.ProblemRequestDto; 7 | import com.may.ars.domain.problem.*; 8 | import com.may.ars.mapper.ReviewMapper; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.time.LocalDate; 16 | import java.time.LocalDateTime; 17 | import java.util.*; 18 | import static java.util.stream.Collectors.toList; 19 | 20 | @Slf4j 21 | @RequiredArgsConstructor 22 | @Service 23 | public class ProblemService { 24 | 25 | private final ProblemRepository problemRepository; 26 | private final TagRepository tagRepository; 27 | private final ProblemQueryRepository problemQueryRepository; 28 | 29 | private final ReviewMapper reviewMapper; 30 | 31 | @Transactional(readOnly = true) 32 | public List getProblemListByStepOrTag(LocalDateTime modifiedDate, Long cursorId, int step, String tagName, Pageable page) { 33 | if (step == 0 && tagName.isBlank()) { 34 | return getProblemList(modifiedDate, cursorId, page); 35 | } 36 | else if (step == 0) { 37 | return getProblemListByTagName(modifiedDate, tagName, cursorId, page); 38 | } 39 | else { 40 | return getProblemList(modifiedDate, cursorId, page); 41 | } 42 | } 43 | 44 | @Transactional(readOnly = true) 45 | public Problem getProblemById(Long problemId) { 46 | return problemRepository.findById(problemId).orElseThrow(EntityNotFoundException::new); 47 | } 48 | 49 | @Transactional(readOnly = true) 50 | public long getProblemCount() { 51 | return problemRepository.count(); 52 | } 53 | 54 | @Transactional 55 | public Long registerProblem(Problem problem, ProblemRequestDto registerDto) { 56 | Review review = reviewMapper.toEntity(problem, registerDto); 57 | 58 | List tagList = registerDto.getTagList().stream() 59 | .map(tagName -> tagRepository.findByTagName(tagName) 60 | .map( 61 | tag -> new ProblemTag(problem, tag)).orElseGet( 62 | () -> new ProblemTag(problem, new Tag(tagName)) 63 | )).collect(toList()); 64 | 65 | problem.setReviewAndTagList(review, tagList); 66 | problemRepository.save(problem); 67 | 68 | return problem.getId(); 69 | } 70 | 71 | @Transactional 72 | public void updateStep(Long problemId, Member member, int step) { 73 | Problem updateProblem = checkValidUser(problemId, member); 74 | updateProblem.updateStep(step); 75 | problemRepository.save(updateProblem); 76 | } 77 | 78 | @Transactional 79 | public void updateNotificationDate(Long problemId, Member member, LocalDate notificationDate) { 80 | Problem updateProblem = checkValidUser(problemId, member); 81 | updateProblem.updateNotificationDate(notificationDate); 82 | problemRepository.save(updateProblem); 83 | } 84 | 85 | @Transactional 86 | public void deleteProblem(Long problemId, Member member) { 87 | checkValidUser(problemId, member); 88 | problemRepository.deleteById(problemId); 89 | } 90 | 91 | public List getProblemList(LocalDateTime modifiedDate, Long cursorId, Pageable page) { 92 | if (modifiedDate == null) { 93 | return problemRepository.findByOrderByModifiedDateDesc(page); 94 | } 95 | return problemRepository.findByModifiedDateBeforeAndIdNotOrderByModifiedDateDesc(modifiedDate, cursorId, page); 96 | } 97 | 98 | public List getProblemListByStep(int step, Long cursorId, Pageable page) { 99 | return cursorId.equals(0L) ? 100 | problemRepository.findAllByStepOrderByModifiedDateDesc(step, page) : 101 | problemRepository.findByIdLessThanAndStepOrderByIdDesc(cursorId, step, page); // 커서기반 페이징 102 | } 103 | 104 | public List getProblemListByTagName(LocalDateTime modifiedDate, String tagName, Long cursorId, Pageable page) { 105 | if (tagName == null) { 106 | return getProblemList(modifiedDate, cursorId, page); 107 | } 108 | return problemQueryRepository.findAllByTag(modifiedDate, cursorId, tagName, page.getPageSize()); 109 | } 110 | 111 | private Problem checkValidUser(Long problemId, Member member) { 112 | return problemRepository.findProblemByIdAndWriter(problemId, member).orElseThrow(EntityNotFoundException::new); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/may/ars/controller/ProblemApiController.java: -------------------------------------------------------------------------------- 1 | package com.may.ars.controller; 2 | 3 | import com.may.ars.domain.member.Member; 4 | import com.may.ars.dto.common.ResponseDto; 5 | import com.may.ars.dto.problem.request.ProblemNotificationUpdateDto; 6 | import com.may.ars.dto.problem.request.ProblemRequestDto; 7 | import com.may.ars.dto.problem.request.ProblemStepUpdateDto; 8 | import com.may.ars.dto.problem.response.ProblemOnlyDto; 9 | import com.may.ars.mapper.ProblemMapper; 10 | import com.may.ars.common.message.SuccessMessage; 11 | import com.may.ars.service.ProblemService; 12 | import com.may.ars.utils.auth.AuthCheck; 13 | import com.may.ars.utils.auth.MemberContext; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.data.domain.PageRequest; 17 | import org.springframework.format.annotation.DateTimeFormat; 18 | import org.springframework.http.HttpStatus; 19 | import org.springframework.http.ResponseEntity; 20 | import org.springframework.web.bind.annotation.*; 21 | 22 | import javax.validation.Valid; 23 | import java.time.LocalDateTime; 24 | import java.util.List; 25 | import java.util.stream.Collectors; 26 | 27 | import static com.may.ars.common.message.SuccessMessage.*; 28 | import static java.util.stream.Collectors.toList; 29 | 30 | @Slf4j 31 | @RequiredArgsConstructor 32 | @RestController 33 | @RequestMapping("/api/problems") 34 | public class ProblemApiController { 35 | 36 | private final ProblemService problemService; 37 | 38 | private final ProblemMapper problemMapper; 39 | 40 | @GetMapping 41 | public ResponseEntity getProblemList(@RequestParam(value = "step", defaultValue = "0") int step, 42 | @RequestParam(value = "tag", defaultValue = "") String tagName, 43 | @RequestParam(value = "cursorId", defaultValue = "0") Long cursorId, 44 | @RequestParam(value = "timestamp", required = false) 45 | @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime modifiedDate, 46 | @RequestParam(value = "size", defaultValue = "12") int size){ 47 | // PageRequest.of()의 첫 번째 파라미터는 무조건 0으로, 즉 최초의 페이지로 처리를 해야 한다. 48 | List problemList = problemService.getProblemListByStepOrTag(modifiedDate, cursorId, step, tagName, PageRequest.of(0, size)) 49 | .stream().map(problemMapper::toReviewExcludeDto).collect(toList()); 50 | return ResponseEntity.ok().body(ResponseDto.of( 51 | HttpStatus.OK, SUCCESS_GET_PROBLEM_LIST, problemList) 52 | ); 53 | } 54 | 55 | @GetMapping("/{problemId}") 56 | public ResponseEntity getProblem(@PathVariable Long problemId) { 57 | return ResponseEntity.ok().body(ResponseDto.of( 58 | HttpStatus.OK, SUCCESS_GET_PROBLEM, problemMapper.toDto(problemService.getProblemById(problemId))) 59 | ); 60 | } 61 | 62 | @GetMapping("/count") 63 | public ResponseEntity getProblemCount() { 64 | return ResponseEntity.ok(ResponseDto.of( 65 | HttpStatus.OK, SUCCESS_GET_PROBLEM_COUNT, problemService.getProblemCount() 66 | )); 67 | } 68 | 69 | @AuthCheck 70 | @PostMapping 71 | public ResponseEntity saveProblem(@RequestBody @Valid ProblemRequestDto requestDto) { 72 | Member member = MemberContext.currentMember.get(); 73 | 74 | return ResponseEntity.ok().body(ResponseDto.of( 75 | HttpStatus.OK, 76 | SuccessMessage.SUCCESS_REGISTER_PROBLEM, 77 | problemService.registerProblem(problemMapper.toEntity(requestDto, member), requestDto) 78 | )); 79 | } 80 | 81 | @AuthCheck 82 | @PutMapping("/{problemId}/step") 83 | public ResponseEntity updateStep(@PathVariable Long problemId, @RequestBody @Valid ProblemStepUpdateDto updateDto) { 84 | Member member = MemberContext.currentMember.get(); 85 | problemService.updateStep(problemId, member, updateDto.getStep()); 86 | 87 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_UPDATE_PROBLEM)); 88 | } 89 | 90 | @AuthCheck 91 | @PutMapping("/{problemId}/notification") 92 | public ResponseEntity updateNotificationDate(@PathVariable Long problemId, @RequestBody @Valid ProblemNotificationUpdateDto updateDto) { 93 | Member member = MemberContext.currentMember.get(); 94 | problemService.updateNotificationDate(problemId, member, updateDto.getNotificationDate()); 95 | 96 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_UPDATE_PROBLEM)); 97 | } 98 | 99 | @DeleteMapping("/{problemId}") 100 | public ResponseEntity deleteProblem(@PathVariable Long problemId) { 101 | Member member = MemberContext.currentMember.get(); 102 | problemService.deleteProblem(problemId, member); 103 | 104 | return ResponseEntity.ok().body(ResponseDto.of(HttpStatus.OK, SUCCESS_DELETE_PROBLEM)); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | --------------------------------------------------------------------------------